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 +4 -4
- data/.envrc +5 -0
- data/.gitignore +1 -0
- data/.s3_sync.sample +2 -0
- data/Changelog.md +68 -0
- data/README.md +17 -10
- data/WARP.md +112 -0
- data/lib/middleman/s3_sync/cloudfront.rb +27 -8
- data/lib/middleman/s3_sync/resource.rb +4 -6
- data/lib/middleman/s3_sync/version.rb +1 -1
- data/lib/middleman/s3_sync.rb +3 -3
- data/lib/middleman-s3_sync/commands.rb +10 -0
- data/lib/middleman-s3_sync/extension.rb +2 -0
- data/spec/aws_sdk_parameters_spec.rb +415 -0
- data/spec/cloudfront_spec.rb +120 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 61870e8a5164a26db62b7463da7be2c74272800ab76cd6eab3fe83ec68e58081
|
4
|
+
data.tar.gz: d901b5c020cc4fe8ddc695ceeb2fde34a3555f4645aca7ec04cea7d513a0e37c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 40ff859c4ae0b722c2132b3192bd14ab454988b612afb18f442645d7fbfb894b48e7d5ba502a1aae36439ec751c8f2f03e8496df255c736c5c2718770f69a7be
|
7
|
+
data.tar.gz: 0a1fe315c71b79ae80041efaf2beb206cdc4caa1db49a7635ede51c8e53c8bbb1cd95a2b0a28cf2e9eb2dcc4c98101e5a6847c3792fc567d6f2b2d26c4bd92f4
|
data/.envrc
ADDED
data/.gitignore
CHANGED
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
|
176
|
-
|
|
177
|
-
| cloudfront_distribution_id
|
178
|
-
| cloudfront_invalidate
|
179
|
-
| cloudfront_invalidate_all
|
180
|
-
| cloudfront_invalidation_batch_size| ```1000``` | Maximum paths per invalidation request |
|
181
|
-
|
|
182
|
-
|
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. **
|
224
|
-
5. **
|
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 =
|
34
|
+
invalidation_id = create_invalidation_with_retry(batch, options)
|
35
35
|
invalidation_ids << invalidation_id if invalidation_id
|
36
36
|
|
37
|
-
# Add a
|
38
|
-
|
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
|
-
|
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[
|
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] = {
|
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[
|
293
|
+
full_s3_resource.metadata['content-md5']
|
296
294
|
end
|
297
295
|
end
|
298
296
|
|
data/lib/middleman/s3_sync.rb
CHANGED
@@ -107,10 +107,10 @@ module Middleman
|
|
107
107
|
|
108
108
|
def update_bucket_website
|
109
109
|
opts = {}
|
110
|
-
opts[:
|
111
|
-
opts[:
|
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[:
|
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
|
data/spec/cloudfront_spec.rb
CHANGED
@@ -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.
|
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-
|
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
|