middleman-s3_sync 4.6.0 → 4.6.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d5baff2d2863949eb480b7ac94d105d3fdcc2c7df67fab66c649f575dd686a4
4
- data.tar.gz: 70c12f42e60eabb0bf063d47a3e42f2766ea883bf47c17557cf323b15a9e546a
3
+ metadata.gz: 61870e8a5164a26db62b7463da7be2c74272800ab76cd6eab3fe83ec68e58081
4
+ data.tar.gz: d901b5c020cc4fe8ddc695ceeb2fde34a3555f4645aca7ec04cea7d513a0e37c
5
5
  SHA512:
6
- metadata.gz: 663ab12a585095543adb3843976e1d24bbf5d3e6bb52a5285b3ce7de5cc7796cb916b72dc330d110225e191325add7e6eed81797abccae9c4e66d0b1d84c8608
7
- data.tar.gz: b9b720083616f701318747b661aa04d9257c9539136ae0a2d153cc314eced337baa7ca158cb6e10de1d86ef1bd29cb90cd49b7ee1160c69bdf7b9bab09d4c27f
6
+ metadata.gz: 40ff859c4ae0b722c2132b3192bd14ab454988b612afb18f442645d7fbfb894b48e7d5ba502a1aae36439ec751c8f2f03e8496df255c736c5c2718770f69a7be
7
+ data.tar.gz: 0a1fe315c71b79ae80041efaf2beb206cdc4caa1db49a7635ede51c8e53c8bbb1cd95a2b0a28cf2e9eb2dcc4c98101e5a6847c3792fc567d6f2b2d26c4bd92f4
data/.envrc ADDED
@@ -0,0 +1,5 @@
1
+ # Adds bin to the path.
2
+ if ! test -d bin || test Gemfile.lock -nt bin; then
3
+ bundle binstubs --all
4
+ fi
5
+ PATH_add bin
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  .aider*
19
+ /bin
data/.s3_sync.sample CHANGED
@@ -20,4 +20,6 @@ cloudfront_distribution_id: <CloudFront Distribution ID> # e.g., E1234567890123
20
20
  cloudfront_invalidate: false # Set to true to enable
21
21
  cloudfront_invalidate_all: false # Set to true to invalidate all paths (/*)
22
22
  cloudfront_invalidation_batch_size: 1000 # Max paths per invalidation request
23
+ cloudfront_invalidation_max_retries: 5 # Max retries for rate-limited requests
24
+ cloudfront_invalidation_batch_delay: 2 # Delay in seconds between invalidation batches
23
25
  cloudfront_wait: false # Set to true to wait for invalidation to complete
data/Changelog.md CHANGED
@@ -2,6 +2,74 @@
2
2
 
3
3
  The gem that tries really hard not to push files to S3.
4
4
 
5
+ ## v4.6.2
6
+
7
+ * Fix AWS SDK parameter format issues from Fog migration
8
+ * Fix website configuration parameters to use symbol keys instead of strings
9
+ * Fix S3 object metadata parameter format to use correct key suffixes
10
+ * Remove obsolete Fog-style constants (CONTENT_MD5_KEY, REDIRECT_KEY)
11
+ * Add comprehensive test suite for AWS SDK parameter validation (18 new tests)
12
+ * Improve compatibility with AWS SDK v3 to prevent API errors
13
+
14
+ ## v4.6.1
15
+
16
+ * Add CloudFront rate limit handling with exponential backoff retry logic
17
+ * Add configurable retry settings: `cloudfront_invalidation_max_retries` and `cloudfront_invalidation_batch_delay`
18
+ * Improve CloudFront error handling for "Rate exceeded" and "Throttling" errors
19
+ * Add command line options for retry configuration
20
+ * Update documentation with retry configuration examples
21
+ * Increase default batch delay from 1 to 2 seconds for better rate limit prevention
22
+
23
+ ## v4.6.0
24
+
25
+ * Add comprehensive CloudFront invalidation support with smart path tracking
26
+ * Add CloudFront configuration options and command line switches
27
+ * Add batch processing for CloudFront invalidations to respect API limits
28
+ * Add path normalization and deduplication for efficient invalidations
29
+ * Add dry-run support for CloudFront invalidations
30
+ * Add wait functionality for CI/CD pipeline integration
31
+ * Update README with CloudFront documentation and best practices
32
+
33
+ ## v4.5.0
34
+
35
+ * Migrate from Fog gem to native AWS SDK S3 client
36
+ * Remove numerous transitive dependencies by dropping Fog
37
+ * Fix path handling inconsistencies with leading slashes
38
+ * Improve resource handling for AWS SDK v3 compatibility
39
+ * Add Ruby 3.2 compatibility fixes
40
+ * Fix build behavior to not auto-build unless explicitly requested
41
+ * Optimize resource processing for better performance
42
+ * Update nokogiri dependency for security
43
+ * Enhance test coverage and mocking
44
+
45
+ ## v4.4.0
46
+
47
+ * Add support for newer Ruby versions (3.0+)
48
+ * Update dependencies for security and compatibility
49
+ * Fix deprecation warnings with newer Ruby versions
50
+ * Improve error handling and logging
51
+
52
+ ## v4.3.0
53
+
54
+ * Enhanced S3 client configuration options
55
+ * Improved AWS credential handling
56
+ * Better support for custom S3 endpoints
57
+ * Performance optimizations for large sites
58
+
59
+ ## v4.2.0
60
+
61
+ * Add support for S3 transfer acceleration
62
+ * Improve concurrent upload handling
63
+ * Enhanced progress reporting
64
+ * Better error messages and debugging
65
+
66
+ ## v4.1.0
67
+
68
+ * Add support for custom content types
69
+ * Improve gzip handling and encoding detection
70
+ * Enhanced caching policy management
71
+ * Better support for redirects and metadata
72
+
5
73
  ## v4.0.1
6
74
 
7
75
  * Fix order of manipulator chain so that S3 Sync is always the last action
data/README.md CHANGED
@@ -172,14 +172,15 @@ end
172
172
 
173
173
  ### Configuration Options
174
174
 
175
- | Setting | Default | Description |
176
- | --------------------------------- | ----------- | ----------- |
177
- | cloudfront_distribution_id | - | CloudFront distribution ID to invalidate |
178
- | cloudfront_invalidate | ```false``` | Enable CloudFront invalidation after sync |
179
- | cloudfront_invalidate_all | ```false``` | Invalidate all paths (/*) instead of only changed files |
180
- | cloudfront_invalidation_batch_size| ```1000``` | Maximum paths per invalidation request |
181
- | cloudfront_wait | ```false``` | Wait for CloudFront invalidation to complete |
182
- </edits>
175
+ | Setting | Default | Description |
176
+ | ------------------------------------- | ----------- | ----------- |
177
+ | cloudfront_distribution_id | - | CloudFront distribution ID to invalidate |
178
+ | cloudfront_invalidate | ```false``` | Enable CloudFront invalidation after sync |
179
+ | cloudfront_invalidate_all | ```false``` | Invalidate all paths (/*) instead of only changed files |
180
+ | cloudfront_invalidation_batch_size | ```1000``` | Maximum paths per invalidation request |
181
+ | cloudfront_invalidation_max_retries | ```5``` | Maximum retries for rate-limited requests |
182
+ | cloudfront_invalidation_batch_delay | ```2``` | Delay in seconds between invalidation batches |
183
+ | cloudfront_wait | ```false``` | Wait for CloudFront invalidation to complete |
183
184
 
184
185
  ### Command Line Options
185
186
 
@@ -198,6 +199,9 @@ middleman s3_sync --cloudfront-invalidate --cloudfront-wait --cloudfront-distrib
198
199
  # Custom batch size for large numbers of files
199
200
  middleman s3_sync --cloudfront-invalidate --cloudfront-invalidation-batch-size 500 --cloudfront-distribution-id E1234567890123
200
201
 
202
+ # Adjust retry behavior for rate limiting
203
+ middleman s3_sync --cloudfront-invalidate --cloudfront-invalidation-max-retries 3 --cloudfront-invalidation-batch-delay 5 --cloudfront-distribution-id E1234567890123
204
+
201
205
  # Short aliases
202
206
  middleman s3_sync -c -d E1234567890123 # Basic invalidation
203
207
  middleman s3_sync -a -d E1234567890123 # Invalidate all paths
@@ -213,6 +217,8 @@ middleman s3_sync -c -a -w -d E1234567890123 # Invalidate all and wait
213
217
  | `--cloudfront-invalidate` | `-c` | Enable CloudFront invalidation |
214
218
  | `--cloudfront-invalidate-all` | `-a` | Invalidate all paths (/*) |
215
219
  | `--cloudfront-invalidation-batch-size` | - | Max paths per request (default: 1000) |
220
+ | `--cloudfront-invalidation-max-retries` | - | Max retries for rate limits (default: 5) |
221
+ | `--cloudfront-invalidation-batch-delay` | - | Delay between batches in seconds (default: 2) |
216
222
  | `--cloudfront-wait` | `-w` | Wait for invalidation to complete |
217
223
 
218
224
  ### How It Works
@@ -220,8 +226,9 @@ middleman s3_sync -c -a -w -d E1234567890123 # Invalidate all and wait
220
226
  1. **Smart Invalidation**: By default, only files that were created, updated, or deleted during the sync are invalidated
221
227
  2. **Path Optimization**: Duplicate paths are removed and redundant paths (covered by wildcards) are eliminated
222
228
  3. **Batch Processing**: Large numbers of paths are split into multiple invalidation requests to respect CloudFront limits
223
- 4. **Error Handling**: Invalidation failures are logged but don't stop the sync process
224
- 5. **Dry Run Support**: Use `--dry-run` to see what would be invalidated without making actual API calls
229
+ 4. **Rate Limit Handling**: Automatic retry with exponential backoff when CloudFront rate limits are hit
230
+ 5. **Error Handling**: Invalidation failures are logged but don't stop the sync process
231
+ 6. **Dry Run Support**: Use `--dry-run` to see what would be invalidated without making actual API calls
225
232
 
226
233
  ### IAM Permissions
227
234
 
data/WARP.md ADDED
@@ -0,0 +1,112 @@
1
+ # WARP.md
2
+
3
+ This file provides guidance to WARP (warp.dev) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ middleman-s3_sync is a Ruby gem that provides intelligent S3 synchronization for Middleman static sites. Unlike other sync tools, it only transfers files that have been added, updated, or deleted, making deployments more efficient. The gem also supports CloudFront cache invalidation and advanced caching policies.
8
+
9
+ ## Key Commands
10
+
11
+ ### Development
12
+ ```bash
13
+ # Install dependencies
14
+ bundle install
15
+
16
+ # Run tests
17
+ bundle exec rspec
18
+
19
+ # Run specific test file
20
+ bundle exec rspec spec/resource_spec.rb
21
+
22
+ # Run tests with verbose output
23
+ bundle exec rspec -fd
24
+
25
+ # Build the gem
26
+ bundle exec rake build
27
+
28
+ # Install the gem locally for testing
29
+ bundle exec rake install
30
+ ```
31
+
32
+ ### Testing & Quality
33
+ ```bash
34
+ # Run all specs
35
+ bundle exec rake spec # or just `rake` (default task)
36
+
37
+ # Run specific test patterns
38
+ bundle exec rspec spec/*_spec.rb --pattern "*policy*"
39
+
40
+ # Check gem build without installing
41
+ gem build middleman-s3_sync.gemspec
42
+ ```
43
+
44
+ ## Architecture
45
+
46
+ ### Core Components
47
+
48
+ **Main Sync Engine** (`lib/middleman/s3_sync.rb`)
49
+ - Central orchestration of sync operations
50
+ - Thread-safe S3 operations using mutexes
51
+ - Parallel processing (8 threads by default) for file operations
52
+ - Tracks CloudFront invalidation paths during sync
53
+
54
+ **Extension Integration** (`lib/middleman-s3_sync/extension.rb`)
55
+ - Middleman extension that hooks into the build process
56
+ - Configurable options with environment variable fallbacks
57
+ - Resource list manipulation to prepare files for sync
58
+
59
+ **CLI Commands** (`lib/middleman-s3_sync/commands.rb`)
60
+ - Thor-based command-line interface
61
+ - Extensive option parsing for CloudFront, AWS credentials, and sync behavior
62
+ - Support for dry-run mode and build-then-sync workflows
63
+
64
+ **Resource Management** (`lib/middleman/s3_sync/resource.rb`)
65
+ - Individual file resource handling and status determination
66
+ - MD5-based change detection and caching policy application
67
+
68
+ **CloudFront Integration** (`lib/middleman/s3_sync/cloudfront.rb`)
69
+ - Intelligent cache invalidation with batch processing
70
+ - Rate limit handling and retry logic
71
+ - Path optimization and wildcard support
72
+
73
+ ### Key Design Patterns
74
+
75
+ - **Thread Safety**: Uses mutexes for bucket and bucket_files operations
76
+ - **Parallel Processing**: Leverages `parallel` gem for concurrent S3 operations
77
+ - **Status-Based Operations**: Resources maintain state (create, update, delete, ignore)
78
+ - **Configuration Cascade**: CLI options override config.rb options override .s3_sync file options override environment variables
79
+
80
+ ### File Status Logic
81
+
82
+ The gem determines what to do with each file by comparing:
83
+ - Local file MD5 hashes vs S3 ETags
84
+ - Presence in local build vs S3 bucket
85
+ - Caching policies and content encoding preferences
86
+
87
+ ## Testing Strategy
88
+
89
+ **RSpec Structure**:
90
+ - `caching_policy_spec.rb`: HTTP caching header generation
91
+ - `cloudfront_spec.rb`: CloudFront invalidation logic (comprehensive, 18k lines)
92
+ - `resource_spec.rb`: Individual file resource operations
93
+ - `s3_sync_integration_spec.rb`: End-to-end sync workflows
94
+
95
+ **Mock Strategy**: Uses AWS SDK stub responses and custom S3 object mocks for isolated testing without real AWS calls.
96
+
97
+ ## Configuration Files
98
+
99
+ - **`.s3_sync`**: YAML configuration file for credentials and options (should be gitignored)
100
+ - **`config.rb`**: Middleman configuration with `activate :s3_sync` block
101
+ - **Environment Variables**: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_BUCKET, etc.
102
+
103
+ ## Common Development Patterns
104
+
105
+ When adding new functionality:
106
+ 1. Add option to `extension.rb` with appropriate defaults
107
+ 2. Add CLI flag to `commands.rb` if user-facing
108
+ 3. Implement core logic in main `s3_sync.rb` module
109
+ 4. Add comprehensive specs following existing patterns
110
+ 5. Update README.md with new configuration options
111
+
112
+ The codebase emphasizes security (credential handling), efficiency (parallel operations), and reliability (comprehensive error handling and dry-run support).
@@ -31,11 +31,12 @@ module Middleman
31
31
  path_batches.each_with_index do |batch, index|
32
32
  say_status "Creating invalidation batch #{index + 1}/#{path_batches.length} (#{batch.length} paths)"
33
33
 
34
- invalidation_id = create_invalidation(batch, options)
34
+ invalidation_id = create_invalidation_with_retry(batch, options)
35
35
  invalidation_ids << invalidation_id if invalidation_id
36
36
 
37
- # Add a small delay between batches to avoid rate limiting
38
- sleep(1) if path_batches.length > 1 && index < path_batches.length - 1
37
+ # Add a delay between batches to avoid rate limiting
38
+ delay = options.cloudfront_invalidation_batch_delay || 2
39
+ sleep(delay) if path_batches.length > 1 && index < path_batches.length - 1
39
40
  end
40
41
 
41
42
  if invalidation_ids.any?
@@ -119,6 +120,29 @@ module Middleman
119
120
  result
120
121
  end
121
122
 
123
+ def create_invalidation_with_retry(paths, options)
124
+ max_retries = options.cloudfront_invalidation_max_retries || 5
125
+ retries = 0
126
+ base_delay = 1
127
+
128
+ begin
129
+ create_invalidation(paths, options)
130
+ rescue Aws::CloudFront::Errors::ServiceError => e
131
+ if (e.message.include?('Rate exceeded') || e.message.include?('Throttling')) && retries < max_retries
132
+ retries += 1
133
+ delay = base_delay * (2 ** (retries - 1)) + rand(1..3) # Exponential backoff with jitter
134
+ say_status "#{ANSI.yellow{"Rate limit hit, retrying in #{delay} seconds..."}} (attempt #{retries}/#{max_retries})"
135
+ sleep(delay)
136
+ retry
137
+ else
138
+ say_status "#{ANSI.red{'Failed to create CloudFront invalidation:'}} #{e.message}"
139
+ say_status "Paths: #{paths.join(', ')}" if options.verbose
140
+ raise e unless options.verbose
141
+ nil
142
+ end
143
+ end
144
+ end
145
+
122
146
  def create_invalidation(paths, options)
123
147
  caller_reference = "middleman-s3_sync-#{Time.now.to_i}-#{SecureRandom.hex(4)}"
124
148
 
@@ -134,11 +158,6 @@ module Middleman
134
158
  })
135
159
 
136
160
  response.invalidation.id
137
- rescue Aws::CloudFront::Errors::ServiceError => e
138
- say_status "#{ANSI.red{'Failed to create CloudFront invalidation:'}} #{e.message}"
139
- say_status "Paths: #{paths.join(', ')}" if options.verbose
140
- raise e unless options.verbose
141
- nil
142
161
  end
143
162
 
144
163
  def cloudfront_client(options)
@@ -3,8 +3,6 @@ module Middleman
3
3
  class Resource
4
4
  attr_accessor :path, :resource, :partial_s3_resource, :full_s3_resource, :content_type, :gzipped, :options
5
5
 
6
- CONTENT_MD5_KEY = 'x-amz-meta-content-md5'
7
- REDIRECT_KEY = 'x-amz-website-redirect-location'
8
6
 
9
7
  include Status
10
8
 
@@ -59,7 +57,7 @@ module Middleman
59
57
  :key => key,
60
58
  :acl => options.acl,
61
59
  :content_type => content_type,
62
- CONTENT_MD5_KEY => local_content_md5
60
+ 'content-md5' => local_content_md5
63
61
  }
64
62
 
65
63
  if caching_policy
@@ -80,7 +78,7 @@ module Middleman
80
78
  end
81
79
 
82
80
  if redirect?
83
- attributes[REDIRECT_KEY] = redirect_url
81
+ attributes['website-redirect-location'] = redirect_url
84
82
  end
85
83
 
86
84
  attributes
@@ -125,7 +123,7 @@ module Middleman
125
123
 
126
124
  # Add metadata if present
127
125
  if local_content_md5
128
- upload_options[:metadata] = { CONTENT_MD5_KEY => local_content_md5 }
126
+ upload_options[:metadata] = { 'content-md5' => local_content_md5 }
129
127
  end
130
128
 
131
129
  # Add redirect if present
@@ -292,7 +290,7 @@ module Middleman
292
290
 
293
291
  def remote_content_md5
294
292
  if full_s3_resource && full_s3_resource.metadata
295
- full_s3_resource.metadata[CONTENT_MD5_KEY]
293
+ full_s3_resource.metadata['content-md5']
296
294
  end
297
295
  end
298
296
 
@@ -1,5 +1,5 @@
1
1
  module Middleman
2
2
  module S3Sync
3
- VERSION = "4.6.0"
3
+ VERSION = "4.6.2"
4
4
  end
5
5
  end
@@ -107,10 +107,10 @@ module Middleman
107
107
 
108
108
  def update_bucket_website
109
109
  opts = {}
110
- opts[:IndexDocument] = s3_sync_options.index_document if s3_sync_options.index_document
111
- opts[:ErrorDocument] = s3_sync_options.error_document if s3_sync_options.error_document
110
+ opts[:index_document] = { suffix: s3_sync_options.index_document } if s3_sync_options.index_document
111
+ opts[:error_document] = { key: s3_sync_options.error_document } if s3_sync_options.error_document
112
112
 
113
- if opts[:ErrorDocument] && !opts[:IndexDocument]
113
+ if opts[:error_document] && !opts[:index_document]
114
114
  raise 'S3 requires `index_document` if `error_document` is specified'
115
115
  end
116
116
 
@@ -84,6 +84,14 @@ module Middleman
84
84
  type: :numeric,
85
85
  desc: 'Maximum number of paths to invalidate in a single request (default: 1000).'
86
86
 
87
+ class_option :cloudfront_invalidation_max_retries,
88
+ type: :numeric,
89
+ desc: 'Maximum number of retries for rate-limited invalidation requests (default: 5).'
90
+
91
+ class_option :cloudfront_invalidation_batch_delay,
92
+ type: :numeric,
93
+ desc: 'Delay in seconds between invalidation batches (default: 2).'
94
+
87
95
  class_option :cloudfront_wait,
88
96
  aliases: '-w',
89
97
  type: :boolean,
@@ -127,6 +135,8 @@ module Middleman
127
135
  s3_sync_options.cloudfront_invalidate = options[:cloudfront_invalidate] if options[:cloudfront_invalidate]
128
136
  s3_sync_options.cloudfront_invalidate_all = options[:cloudfront_invalidate_all] if options[:cloudfront_invalidate_all]
129
137
  s3_sync_options.cloudfront_invalidation_batch_size = options[:cloudfront_invalidation_batch_size] if options[:cloudfront_invalidation_batch_size]
138
+ s3_sync_options.cloudfront_invalidation_max_retries = options[:cloudfront_invalidation_max_retries] if options[:cloudfront_invalidation_max_retries]
139
+ s3_sync_options.cloudfront_invalidation_batch_delay = options[:cloudfront_invalidation_batch_delay] if options[:cloudfront_invalidation_batch_delay]
130
140
  s3_sync_options.cloudfront_wait = options[:cloudfront_wait] if options[:cloudfront_wait]
131
141
 
132
142
  ::Middleman::S3Sync.sync()
@@ -33,6 +33,8 @@ module Middleman
33
33
  option :cloudfront_invalidate, false, 'Whether to invalidate CloudFront cache after sync'
34
34
  option :cloudfront_invalidate_all, false, 'Whether to invalidate all paths (/*) or only changed files'
35
35
  option :cloudfront_invalidation_batch_size, 1000, 'Maximum number of paths to invalidate in a single request'
36
+ option :cloudfront_invalidation_max_retries, 5, 'Maximum number of retries for rate-limited invalidation requests'
37
+ option :cloudfront_invalidation_batch_delay, 2, 'Delay in seconds between invalidation batches'
36
38
  option :cloudfront_wait, false, 'Whether to wait for CloudFront invalidation to complete'
37
39
 
38
40
  expose_to_config :s3_sync_options, :default_caching_policy, :caching_policy
@@ -0,0 +1,415 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'AWS SDK Parameter Validation' do
4
+ let(:options) { Middleman::S3Sync::Options.new }
5
+ let(:s3_client) { instance_double(Aws::S3::Client) }
6
+ let(:s3_resource) { instance_double(Aws::S3::Resource) }
7
+ let(:bucket) { instance_double(Aws::S3::Bucket) }
8
+ let(:s3_object) { instance_double(Aws::S3::Object) }
9
+
10
+ before do
11
+ Middleman::S3Sync.s3_sync_options = options
12
+ options.build_dir = "build"
13
+ options.bucket = "test-bucket"
14
+ options.acl = "public-read"
15
+ options.index_document = "index.html"
16
+ options.error_document = "404.html"
17
+ options.version_bucket = true
18
+
19
+ allow(Aws::S3::Client).to receive(:new).and_return(s3_client)
20
+ allow(Aws::S3::Resource).to receive(:new).and_return(s3_resource)
21
+ allow(s3_resource).to receive(:bucket).and_return(bucket)
22
+ allow(bucket).to receive(:exists?).and_return(true)
23
+ allow(bucket).to receive(:object).and_return(s3_object)
24
+ allow(s3_object).to receive(:put).and_return(true)
25
+
26
+ # Allow Middleman::S3Sync to use our mocked client/bucket
27
+ allow(Middleman::S3Sync).to receive(:s3_client).and_return(s3_client)
28
+ allow(Middleman::S3Sync).to receive(:bucket).and_return(bucket)
29
+ allow(Middleman::S3Sync).to receive(:say_status)
30
+ end
31
+
32
+ describe 'put_bucket_website parameters' do
33
+ it 'uses symbol keys for index_document and error_document' do
34
+ expect(s3_client).to receive(:put_bucket_website) do |params|
35
+ expect(params[:bucket]).to eq("test-bucket")
36
+ expect(params[:website_configuration]).to have_key(:index_document)
37
+ expect(params[:website_configuration]).to have_key(:error_document)
38
+
39
+ # Verify the nested structure uses symbols, not strings
40
+ expect(params[:website_configuration][:index_document]).to have_key(:suffix)
41
+ expect(params[:website_configuration][:error_document]).to have_key(:key)
42
+
43
+ # Verify the values are correct
44
+ expect(params[:website_configuration][:index_document][:suffix]).to eq("index.html")
45
+ expect(params[:website_configuration][:error_document][:key]).to eq("404.html")
46
+ end
47
+
48
+ Middleman::S3Sync.send(:update_bucket_website)
49
+ end
50
+
51
+ context 'when only index_document is set' do
52
+ before do
53
+ options.error_document = nil
54
+ end
55
+
56
+ it 'only includes index_document in website configuration' do
57
+ expect(s3_client).to receive(:put_bucket_website) do |params|
58
+ expect(params[:website_configuration]).to have_key(:index_document)
59
+ expect(params[:website_configuration]).not_to have_key(:error_document)
60
+ end
61
+
62
+ Middleman::S3Sync.send(:update_bucket_website)
63
+ end
64
+ end
65
+
66
+ context 'when neither document is set' do
67
+ before do
68
+ options.index_document = nil
69
+ options.error_document = nil
70
+ end
71
+
72
+ it 'does not call put_bucket_website' do
73
+ expect(s3_client).not_to receive(:put_bucket_website)
74
+
75
+ Middleman::S3Sync.send(:update_bucket_website)
76
+ end
77
+ end
78
+
79
+ context 'when only error_document is set' do
80
+ before do
81
+ options.index_document = nil
82
+ end
83
+
84
+ it 'raises an error because S3 requires index_document if error_document is specified' do
85
+ expect {
86
+ Middleman::S3Sync.send(:update_bucket_website)
87
+ }.to raise_error('S3 requires `index_document` if `error_document` is specified')
88
+ end
89
+ end
90
+ end
91
+
92
+ describe 'put_bucket_versioning parameters' do
93
+ it 'uses correct parameter structure' do
94
+ expect(s3_client).to receive(:put_bucket_versioning) do |params|
95
+ expect(params[:bucket]).to eq("test-bucket")
96
+ expect(params[:versioning_configuration]).to be_a(Hash)
97
+ expect(params[:versioning_configuration][:status]).to eq("Enabled")
98
+ end
99
+
100
+ Middleman::S3Sync.send(:update_bucket_versioning)
101
+ end
102
+
103
+ context 'when version_bucket is false' do
104
+ before do
105
+ options.version_bucket = false
106
+ end
107
+
108
+ it 'does not call put_bucket_versioning' do
109
+ expect(s3_client).not_to receive(:put_bucket_versioning)
110
+
111
+ Middleman::S3Sync.send(:update_bucket_versioning)
112
+ end
113
+ end
114
+ end
115
+
116
+ describe 'S3 object upload parameters' do
117
+ let(:mm_resource) do
118
+ double(
119
+ destination_path: 'test/file.html',
120
+ content_type: 'text/html'
121
+ )
122
+ end
123
+
124
+ let(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
125
+
126
+ before do
127
+ allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
128
+ allow(File).to receive(:exist?).with('build/test/file.html.gz').and_return(false)
129
+ allow(File).to receive(:read).with('build/test/file.html').and_return('test content')
130
+ allow(File).to receive(:directory?).with('build/test/file.html').and_return(false)
131
+ allow(s3_object).to receive(:head).and_return(nil)
132
+ options.dry_run = false
133
+ end
134
+
135
+ it 'uses correct metadata key format' do
136
+ expect(s3_object).to receive(:put) do |upload_options|
137
+ # Verify basic parameters
138
+ expect(upload_options[:body]).to eq('test content')
139
+ expect(upload_options[:content_type]).to eq('text/html')
140
+ expect(upload_options[:acl]).to eq('public-read')
141
+
142
+ # Verify metadata uses correct key format (suffix only, not full header name)
143
+ expect(upload_options[:metadata]).to be_a(Hash)
144
+ expect(upload_options[:metadata]).to have_key('content-md5')
145
+ expect(upload_options[:metadata]).not_to have_key('x-amz-meta-content-md5')
146
+
147
+ # Verify metadata value is the MD5 hash
148
+ expected_md5 = Digest::MD5.hexdigest('test content')
149
+ expect(upload_options[:metadata]['content-md5']).to eq(expected_md5)
150
+ end
151
+
152
+ resource.upload!
153
+ end
154
+
155
+ context 'when gzip is enabled' do
156
+ before do
157
+ options.prefer_gzip = true
158
+ allow(File).to receive(:exist?).with('build/test/file.html.gz').and_return(true)
159
+ allow(File).to receive(:read).with('build/test/file.html.gz').and_return('gzipped content')
160
+ allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
161
+ allow(File).to receive(:read).with('build/test/file.html').and_return('original content')
162
+
163
+ # Mock the HEAD response to avoid calling it during redirect?
164
+ head_response = double(
165
+ metadata: {},
166
+ etag: '"abc123"',
167
+ content_encoding: nil,
168
+ cache_control: nil,
169
+ website_redirect_location: nil
170
+ )
171
+ allow(s3_object).to receive(:head).and_return(head_response)
172
+ resource.instance_variable_set(:@full_s3_resource, head_response)
173
+ end
174
+
175
+ it 'includes content_encoding parameter' do
176
+ expect(s3_object).to receive(:put) do |upload_options|
177
+ expect(upload_options[:content_encoding]).to eq('gzip')
178
+ end
179
+
180
+ resource.upload!
181
+ end
182
+ end
183
+
184
+ context 'when reduced redundancy storage is enabled' do
185
+ before do
186
+ options.reduced_redundancy_storage = true
187
+ end
188
+
189
+ it 'includes storage_class parameter' do
190
+ expect(s3_object).to receive(:put) do |upload_options|
191
+ expect(upload_options[:storage_class]).to eq('REDUCED_REDUNDANCY')
192
+ end
193
+
194
+ resource.upload!
195
+ end
196
+ end
197
+
198
+ context 'when encryption is enabled' do
199
+ before do
200
+ options.encryption = true
201
+ end
202
+
203
+ it 'includes server_side_encryption parameter' do
204
+ expect(s3_object).to receive(:put) do |upload_options|
205
+ expect(upload_options[:server_side_encryption]).to eq('AES256')
206
+ end
207
+
208
+ resource.upload!
209
+ end
210
+ end
211
+
212
+ context 'when resource has a redirect' do
213
+ let(:mm_resource) do
214
+ double(
215
+ destination_path: 'redirect/file.html',
216
+ content_type: 'text/html',
217
+ redirect?: true,
218
+ target_url: 'https://example.com/new-location'
219
+ )
220
+ end
221
+
222
+ before do
223
+ allow(File).to receive(:exist?).with('build/redirect/file.html').and_return(true)
224
+ allow(File).to receive(:exist?).with('build/redirect/file.html.gz').and_return(false)
225
+ allow(File).to receive(:read).with('build/redirect/file.html').and_return('redirect content')
226
+ allow(File).to receive(:directory?).with('build/redirect/file.html').and_return(false)
227
+ allow(s3_object).to receive(:head).and_return(nil)
228
+ allow(resource).to receive(:redirect?).and_return(true)
229
+ allow(resource).to receive(:redirect_url).and_return('https://example.com/new-location')
230
+ end
231
+
232
+ it 'includes website_redirect_location parameter' do
233
+ expect(s3_object).to receive(:put) do |upload_options|
234
+ expect(upload_options[:website_redirect_location]).to eq('https://example.com/new-location')
235
+ end
236
+
237
+ resource.upload!
238
+ end
239
+ end
240
+ end
241
+
242
+ describe 'S3 object metadata retrieval' do
243
+ let(:mm_resource) do
244
+ double(
245
+ destination_path: 'test/file.html',
246
+ content_type: 'text/html'
247
+ )
248
+ end
249
+
250
+ let(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
251
+ let(:head_response) do
252
+ double(
253
+ metadata: { 'content-md5' => 'abc123def456' },
254
+ etag: '"def456abc123"',
255
+ content_encoding: nil,
256
+ cache_control: nil,
257
+ website_redirect_location: nil
258
+ )
259
+ end
260
+
261
+ before do
262
+ allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
263
+ allow(File).to receive(:read).with('build/test/file.html').and_return('test content')
264
+ allow(File).to receive(:directory?).with('build/test/file.html').and_return(false)
265
+ allow(s3_object).to receive(:head).and_return(head_response)
266
+ resource.instance_variable_set(:@full_s3_resource, head_response)
267
+ end
268
+
269
+ it 'reads metadata using correct key format' do
270
+ expect(resource.remote_content_md5).to eq('abc123def456')
271
+ end
272
+
273
+ it 'does not try to read metadata with old header format' do
274
+ # Ensure it's not looking for the full header name
275
+ expect(head_response.metadata).not_to receive(:[]).with('x-amz-meta-content-md5')
276
+
277
+ resource.remote_content_md5
278
+ end
279
+ end
280
+
281
+ describe 'to_h method for legacy compatibility' do
282
+ let(:mm_resource) do
283
+ double(
284
+ destination_path: 'test/file.html',
285
+ content_type: 'text/html'
286
+ )
287
+ end
288
+
289
+ let(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
290
+
291
+ before do
292
+ allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
293
+ allow(File).to receive(:exist?).with('build/test/file.html.gz').and_return(false)
294
+ allow(File).to receive(:read).with('build/test/file.html').and_return('test content')
295
+ allow(File).to receive(:directory?).with('build/test/file.html').and_return(false)
296
+ allow(s3_object).to receive(:head).and_return(nil)
297
+ end
298
+
299
+ it 'returns attributes with correct key formats' do
300
+ attributes = resource.to_h
301
+
302
+ expect(attributes[:key]).to eq('test/file.html')
303
+ expect(attributes[:acl]).to eq('public-read')
304
+ expect(attributes[:content_type]).to eq('text/html')
305
+ expect(attributes['content-md5']).to be_a(String)
306
+ expect(attributes).not_to have_key('x-amz-meta-content-md5')
307
+ end
308
+
309
+ context 'when resource has a redirect' do
310
+ let(:mm_resource) do
311
+ double(
312
+ destination_path: 'redirect/file.html',
313
+ content_type: 'text/html',
314
+ redirect?: true,
315
+ target_url: 'https://example.com/new-location'
316
+ )
317
+ end
318
+
319
+ before do
320
+ allow(File).to receive(:exist?).with('build/redirect/file.html').and_return(true)
321
+ allow(File).to receive(:exist?).with('build/redirect/file.html.gz').and_return(false)
322
+ allow(File).to receive(:read).with('build/redirect/file.html').and_return('redirect content')
323
+ allow(File).to receive(:directory?).with('build/redirect/file.html').and_return(false)
324
+ allow(s3_object).to receive(:head).and_return(nil)
325
+ allow(resource).to receive(:redirect?).and_return(true)
326
+ allow(resource).to receive(:redirect_url).and_return('https://example.com/new-location')
327
+ end
328
+
329
+ it 'includes redirect with correct key format' do
330
+ attributes = resource.to_h
331
+
332
+ expect(attributes['website-redirect-location']).to eq('https://example.com/new-location')
333
+ expect(attributes).not_to have_key('x-amz-website-redirect-location')
334
+ end
335
+ end
336
+ end
337
+
338
+ describe 'Regression tests for Fog-style parameter issues' do
339
+ # These tests validate that we've fixed the old Fog-style parameter formatting
340
+ # and demonstrate what would fail if we reverted to the old style
341
+
342
+ it 'does not use string keys for website configuration (old Fog style)' do
343
+ # This would fail if we reverted to the old format:
344
+ # opts[:index_document] = { "suffix" => s3_sync_options.index_document }
345
+
346
+ expect(s3_client).to receive(:put_bucket_website) do |params|
347
+ config = params[:website_configuration]
348
+
349
+ # Ensure we're not using string keys (old Fog style)
350
+ expect(config[:index_document]).not_to have_key("suffix")
351
+ expect(config[:error_document]).not_to have_key("key")
352
+
353
+ # Ensure we ARE using symbol keys (correct AWS SDK style)
354
+ expect(config[:index_document]).to have_key(:suffix)
355
+ expect(config[:error_document]).to have_key(:key)
356
+ end
357
+
358
+ Middleman::S3Sync.send(:update_bucket_website)
359
+ end
360
+
361
+ it 'does not use full header names in metadata (old style)' do
362
+ mm_resource = double(
363
+ destination_path: 'test/file.html',
364
+ content_type: 'text/html'
365
+ )
366
+ resource = Middleman::S3Sync::Resource.new(mm_resource, nil)
367
+
368
+ allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
369
+ allow(File).to receive(:exist?).with('build/test/file.html.gz').and_return(false)
370
+ allow(File).to receive(:read).with('build/test/file.html').and_return('test content')
371
+ allow(File).to receive(:directory?).with('build/test/file.html').and_return(false)
372
+ allow(s3_object).to receive(:head).and_return(nil)
373
+ options.dry_run = false
374
+
375
+ expect(s3_object).to receive(:put) do |upload_options|
376
+ # Ensure we're not using the old full header format
377
+ expect(upload_options[:metadata]).not_to have_key('x-amz-meta-content-md5')
378
+
379
+ # Ensure we ARE using the correct suffix-only format
380
+ expect(upload_options[:metadata]).to have_key('content-md5')
381
+ end
382
+
383
+ resource.upload!
384
+ end
385
+
386
+ it 'validates that old constants are no longer used' do
387
+ # This test ensures the old constants were removed/changed
388
+ # If they still existed, this would be a sign we didn't clean up properly
389
+
390
+ mm_resource = double(
391
+ destination_path: 'test/file.html',
392
+ content_type: 'text/html'
393
+ )
394
+ resource = Middleman::S3Sync::Resource.new(mm_resource, nil)
395
+
396
+ allow(File).to receive(:exist?).with('build/test/file.html').and_return(true)
397
+ allow(File).to receive(:exist?).with('build/test/file.html.gz').and_return(false)
398
+ allow(File).to receive(:read).with('build/test/file.html').and_return('test content')
399
+ allow(File).to receive(:directory?).with('build/test/file.html').and_return(false)
400
+ allow(s3_object).to receive(:head).and_return(nil)
401
+ allow(resource).to receive(:redirect?).and_return(true)
402
+ allow(resource).to receive(:redirect_url).and_return('https://example.com/redirect')
403
+
404
+ attributes = resource.to_h
405
+
406
+ # Validate that the old constant values are not used
407
+ expect(attributes).not_to have_key('x-amz-meta-content-md5')
408
+ expect(attributes).not_to have_key('x-amz-website-redirect-location')
409
+
410
+ # Validate that the correct formats are used
411
+ expect(attributes).to have_key('content-md5')
412
+ expect(attributes).to have_key('website-redirect-location')
413
+ end
414
+ end
415
+ end
@@ -8,6 +8,8 @@ describe Middleman::S3Sync::CloudFront do
8
8
  cloudfront_distribution_id: 'E1234567890123',
9
9
  cloudfront_invalidate_all: false,
10
10
  cloudfront_invalidation_batch_size: 1000,
11
+ cloudfront_invalidation_max_retries: 5,
12
+ cloudfront_invalidation_batch_delay: 2,
11
13
  cloudfront_wait: false,
12
14
  aws_access_key_id: 'test_key',
13
15
  aws_secret_access_key: 'test_secret',
@@ -92,6 +94,8 @@ describe Middleman::S3Sync::CloudFront do
92
94
  cloudfront_distribution_id: 'E1234567890123',
93
95
  cloudfront_invalidate_all: true,
94
96
  cloudfront_invalidation_batch_size: 1000,
97
+ cloudfront_invalidation_max_retries: 5,
98
+ cloudfront_invalidation_batch_delay: 2,
95
99
  cloudfront_wait: false,
96
100
  aws_access_key_id: 'test_key',
97
101
  aws_secret_access_key: 'test_secret',
@@ -201,6 +205,8 @@ describe Middleman::S3Sync::CloudFront do
201
205
  cloudfront_distribution_id: 'E1234567890123',
202
206
  cloudfront_invalidate_all: false,
203
207
  cloudfront_invalidation_batch_size: 2,
208
+ cloudfront_invalidation_max_retries: 5,
209
+ cloudfront_invalidation_batch_delay: 1,
204
210
  cloudfront_wait: false,
205
211
  aws_access_key_id: 'test_key',
206
212
  aws_secret_access_key: 'test_secret',
@@ -239,6 +245,112 @@ describe Middleman::S3Sync::CloudFront do
239
245
  }.to raise_error(Aws::CloudFront::Errors::ServiceError)
240
246
  end
241
247
 
248
+ context 'when rate limit is exceeded' do
249
+ let(:rate_error) { Aws::CloudFront::Errors::ServiceError.new(nil, 'Rate exceeded') }
250
+
251
+ it 'retries with exponential backoff' do
252
+ client = double('cloudfront_client')
253
+ allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
254
+
255
+ # Fail twice with rate limit, then succeed
256
+ call_count = 0
257
+ allow(client).to receive(:create_invalidation) do
258
+ call_count += 1
259
+ if call_count <= 2
260
+ raise rate_error
261
+ else
262
+ invalidation_response
263
+ end
264
+ end
265
+
266
+ # Allow normal status messages but expect retry messages
267
+ allow(described_class).to receive(:say_status)
268
+ expect(described_class).to receive(:say_status).with(
269
+ match(/Rate limit hit, retrying in \d+ seconds.*attempt 1\/5/)
270
+ ).ordered
271
+ expect(described_class).to receive(:say_status).with(
272
+ match(/Rate limit hit, retrying in \d+ seconds.*attempt 2\/5/)
273
+ ).ordered
274
+
275
+ # Expect sleep calls for backoff
276
+ expect(described_class).to receive(:sleep).twice
277
+
278
+ result = described_class.invalidate(['/path1'], options)
279
+ expect(result).to eq(['I1234567890123'])
280
+ end
281
+
282
+ it 'gives up after max retries and raises error' do
283
+ rate_limited_options = double(
284
+ cloudfront_invalidate: true,
285
+ cloudfront_distribution_id: 'E1234567890123',
286
+ cloudfront_invalidate_all: false,
287
+ cloudfront_invalidation_batch_size: 1000,
288
+ cloudfront_invalidation_max_retries: 2,
289
+ cloudfront_invalidation_batch_delay: 2,
290
+ cloudfront_wait: false,
291
+ aws_access_key_id: 'test_key',
292
+ aws_secret_access_key: 'test_secret',
293
+ aws_session_token: nil,
294
+ dry_run: false,
295
+ verbose: false
296
+ )
297
+
298
+ client = double('cloudfront_client')
299
+ allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
300
+
301
+ # Fail max_retries + 1 times
302
+ expect(client).to receive(:create_invalidation).exactly(3).times.and_raise(rate_error)
303
+
304
+ # Allow normal status messages
305
+ allow(described_class).to receive(:say_status)
306
+
307
+ # Expect retry status messages
308
+ expect(described_class).to receive(:say_status).with(
309
+ match(/Rate limit hit, retrying in \d+ seconds.*attempt 1\/2/)
310
+ )
311
+ expect(described_class).to receive(:say_status).with(
312
+ match(/Rate limit hit, retrying in \d+ seconds.*attempt 2\/2/)
313
+ )
314
+ expect(described_class).to receive(:say_status).with(
315
+ match(/Failed to create CloudFront invalidation.*Rate exceeded/)
316
+ )
317
+
318
+ # Expect sleep calls for backoff
319
+ expect(described_class).to receive(:sleep).twice
320
+
321
+ expect {
322
+ described_class.invalidate(['/path1'], rate_limited_options)
323
+ }.to raise_error(Aws::CloudFront::Errors::ServiceError)
324
+ end
325
+
326
+ it 'handles throttling errors the same as rate exceeded' do
327
+ throttling_error = Aws::CloudFront::Errors::ServiceError.new(nil, 'Throttling: Request was throttled')
328
+
329
+ client = double('cloudfront_client')
330
+ allow(Aws::CloudFront::Client).to receive(:new).and_return(client)
331
+
332
+ call_count = 0
333
+ allow(client).to receive(:create_invalidation) do
334
+ call_count += 1
335
+ if call_count == 1
336
+ raise throttling_error
337
+ else
338
+ invalidation_response
339
+ end
340
+ end
341
+
342
+ # Allow normal status messages but expect retry message
343
+ allow(described_class).to receive(:say_status)
344
+ expect(described_class).to receive(:say_status).with(
345
+ match(/Rate limit hit, retrying in \d+ seconds.*attempt 1\/5/)
346
+ ).ordered
347
+ expect(described_class).to receive(:sleep).once
348
+
349
+ result = described_class.invalidate(['/path1'], options)
350
+ expect(result).to eq(['I1234567890123'])
351
+ end
352
+ end
353
+
242
354
  context 'when verbose mode is enabled' do
243
355
  let(:options) do
244
356
  double(
@@ -246,6 +358,8 @@ describe Middleman::S3Sync::CloudFront do
246
358
  cloudfront_distribution_id: 'E1234567890123',
247
359
  cloudfront_invalidate_all: false,
248
360
  cloudfront_invalidation_batch_size: 1000,
361
+ cloudfront_invalidation_max_retries: 5,
362
+ cloudfront_invalidation_batch_delay: 2,
249
363
  cloudfront_wait: false,
250
364
  aws_access_key_id: 'test_key',
251
365
  aws_secret_access_key: 'test_secret',
@@ -285,6 +399,8 @@ describe Middleman::S3Sync::CloudFront do
285
399
  cloudfront_distribution_id: 'E1234567890123',
286
400
  cloudfront_invalidate_all: false,
287
401
  cloudfront_invalidation_batch_size: 1000,
402
+ cloudfront_invalidation_max_retries: 5,
403
+ cloudfront_invalidation_batch_delay: 2,
288
404
  cloudfront_wait: true,
289
405
  aws_access_key_id: 'test_key',
290
406
  aws_secret_access_key: 'test_secret',
@@ -336,6 +452,8 @@ describe Middleman::S3Sync::CloudFront do
336
452
  cloudfront_distribution_id: 'E1234567890123',
337
453
  cloudfront_invalidate_all: false,
338
454
  cloudfront_invalidation_batch_size: 1000,
455
+ cloudfront_invalidation_max_retries: 5,
456
+ cloudfront_invalidation_batch_delay: 2,
339
457
  cloudfront_wait: false,
340
458
  aws_access_key_id: 'test_key',
341
459
  aws_secret_access_key: 'test_secret',
@@ -367,6 +485,8 @@ describe Middleman::S3Sync::CloudFront do
367
485
  cloudfront_distribution_id: 'E1234567890123',
368
486
  cloudfront_invalidate_all: false,
369
487
  cloudfront_invalidation_batch_size: 1000,
488
+ cloudfront_invalidation_max_retries: 5,
489
+ cloudfront_invalidation_batch_delay: 2,
370
490
  cloudfront_wait: false,
371
491
  aws_access_key_id: nil,
372
492
  aws_secret_access_key: nil,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: middleman-s3_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.6.0
4
+ version: 4.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frederic Jean
8
8
  - Will Koehler
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-26 00:00:00.000000000 Z
11
+ date: 2025-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: middleman-core
@@ -311,6 +311,7 @@ executables: []
311
311
  extensions: []
312
312
  extra_rdoc_files: []
313
313
  files:
314
+ - ".envrc"
314
315
  - ".gitignore"
315
316
  - ".rspec"
316
317
  - ".s3_sync.sample"
@@ -320,6 +321,7 @@ files:
320
321
  - LICENSE.txt
321
322
  - README.md
322
323
  - Rakefile
324
+ - WARP.md
323
325
  - lib/middleman-s3_sync.rb
324
326
  - lib/middleman-s3_sync/commands.rb
325
327
  - lib/middleman-s3_sync/extension.rb
@@ -333,6 +335,7 @@ files:
333
335
  - lib/middleman/s3_sync/version.rb
334
336
  - lib/middleman_extension.rb
335
337
  - middleman-s3_sync.gemspec
338
+ - spec/aws_sdk_parameters_spec.rb
336
339
  - spec/caching_policy_spec.rb
337
340
  - spec/cloudfront_spec.rb
338
341
  - spec/resource_spec.rb
@@ -360,6 +363,7 @@ rubygems_version: 3.6.2
360
363
  specification_version: 4
361
364
  summary: Tries really, really hard not to push files to S3.
362
365
  test_files:
366
+ - spec/aws_sdk_parameters_spec.rb
363
367
  - spec/caching_policy_spec.rb
364
368
  - spec/cloudfront_spec.rb
365
369
  - spec/resource_spec.rb