middleman-s3_sync 4.0.3 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e3827f761cc837c4b40aab6fd29a7737fc7039b1
4
- data.tar.gz: 3c42ac240f148cb08d7daa3e72dacb29c11c59c0
2
+ SHA256:
3
+ metadata.gz: 7d5baff2d2863949eb480b7ac94d105d3fdcc2c7df67fab66c649f575dd686a4
4
+ data.tar.gz: 70c12f42e60eabb0bf063d47a3e42f2766ea883bf47c17557cf323b15a9e546a
5
5
  SHA512:
6
- metadata.gz: edbf4fb9046f29261590403220112597f01221350ac223b815856d7a89f833969fbf2a9eb9cb430a9241ffc37603e8baa049baa6ddc64ac1f5b62c6205773453
7
- data.tar.gz: 370d1524220e776febbce93868341fc2d381163768dbfb7b698c64e802625f495aefa0ff833735c270362d9c16f0d812523c517ac0ea05c71c7551e3c8b93efe
6
+ metadata.gz: 663ab12a585095543adb3843976e1d24bbf5d3e6bb52a5285b3ce7de5cc7796cb916b72dc330d110225e191325add7e6eed81797abccae9c4e66d0b1d84c8608
7
+ data.tar.gz: b9b720083616f701318747b661aa04d9257c9539136ae0a2d153cc314eced337baa7ca158cb6e10de1d86ef1bd29cb90cd49b7ee1160c69bdf7b9bab09d4c27f
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .aider*
data/.s3_sync.sample CHANGED
@@ -1,3 +1,23 @@
1
1
  ---
2
2
  aws_access_key_id: <AWS Access Key>
3
3
  aws_secret_access_key: <AWS Secret Access Key>
4
+ bucket: <S3 Bucket Name>
5
+ region: us-east-1
6
+ delete: true
7
+ after_build: false
8
+ prefer_gzip: true
9
+ path_style: true
10
+ reduced_redundancy_storage: false
11
+ acl: public-read
12
+ encryption: false
13
+ prefix: ''
14
+ version_bucket: false
15
+ index_document: index.html
16
+ error_document: 404.html
17
+
18
+ # CloudFront Invalidation Settings
19
+ cloudfront_distribution_id: <CloudFront Distribution ID> # e.g., E1234567890123
20
+ cloudfront_invalidate: false # Set to true to enable
21
+ cloudfront_invalidate_all: false # Set to true to invalidate all paths (/*)
22
+ cloudfront_invalidation_batch_size: 1000 # Max paths per invalidation request
23
+ cloudfront_wait: false # Set to true to wait for invalidation to complete
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Middleman::S3Sync
2
2
 
3
- [![Join the chat at https://gitter.im/fredjean/middleman-s3_sync](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/fredjean/middleman-s3_sync?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Code Climate](https://codeclimate.com/github/fredjean/middleman-s3_sync.png)](https://codeclimate.com/github/fredjean/middleman-s3_sync) [![Build Status](https://travis-ci.org/fredjean/middleman-s3_sync.png?branch=master)](https://travis-ci.org/fredjean/middleman-s3_sync)
3
+ [![Join the chat at https://gitter.im/fredjean/middleman-s3_sync](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/fredjean/middleman-s3_sync?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Code Climate](https://codeclimate.com/github/fredjean/middleman-s3_sync.svg)](https://codeclimate.com/github/fredjean/middleman-s3_sync) [![Build Status](https://travis-ci.org/fredjean/middleman-s3_sync.svg?branch=master)](https://travis-ci.org/fredjean/middleman-s3_sync)
4
4
 
5
5
  This gem determines which files need to be added, updated and optionally deleted
6
6
  and only transfer these files up. This reduces the impact of an update
@@ -54,6 +54,9 @@ activate :s3_sync do |s3_sync|
54
54
  s3_sync.version_bucket = false
55
55
  s3_sync.index_document = 'index.html'
56
56
  s3_sync.error_document = '404.html'
57
+ s3_sync.cloudfront_distribution_id = 'E1234567890123' # CloudFront distribution ID
58
+ s3_sync.cloudfront_invalidate = false # Enable CloudFront invalidation
59
+ s3_sync.cloudfront_invalidate_all = false # Invalidate all paths (/*) or only changed files
57
60
  end
58
61
  ```
59
62
 
@@ -76,81 +79,254 @@ The following defaults apply to the configuration items:
76
79
  | encryption | ```false``` |
77
80
  | acl | ```'public-read'``` |
78
81
  | version_bucket | ```false``` |
82
+ | cloudfront_distribution_id | - |
83
+ | cloudfront_invalidate | ```false``` |
84
+ | cloudfront_invalidate_all | ```false``` |
85
+ | cloudfront_wait | ```false``` |
79
86
 
80
87
  ## Setting AWS Credentials
81
88
 
82
- There are several ways to provide the AWS credentials for s3_sync:
89
+ There are several secure ways to provide AWS credentials for s3_sync. Using temporary, least-privilege credentials is strongly recommended.
83
90
 
84
- #### Through `config.rb`
91
+ #### Best Practices for AWS Credentials (Recommended)
85
92
 
86
- You can set the aws_access_key_id and aws_secret_access_key in the block
87
- that is passed to the activate method.
93
+ ##### 1. AWS IAM Roles (Most Secure)
88
94
 
89
- > I strongly discourage using this method. This will lead you to add and commit these changes
90
- > to your SCM and potentially expose sensitive information to the world.
95
+ ###### For CI/CD and Cloud Environments
96
+ - **EC2 Instance Profiles**: If running on EC2, use IAM roles attached to your instance. Credentials are automatically rotated and managed by AWS.
97
+ - **ECS Task Roles**: For container workloads, use task roles to provide permissions to specific containers.
98
+ - **CI/CD Service Roles**: Most CI/CD services (GitHub Actions, CircleCI, etc.) offer native AWS integrations that support assuming IAM roles.
91
99
 
92
- #### Through `.s3_sync` File
100
+ ###### For Local Development
101
+ - **AWS IAM Identity Center (SSO)** and configured profiles in your AWS config file
102
+ - **AWS CLI credential process** to integrate with external identity providers
103
+ - **Role assumption** with short-lived credentials through `aws sts assume-role`
93
104
 
94
- You can create a `.s3_sync` at the root of your middleman project.
95
- The credentials are passed in the YAML format. The keys match the
96
- options keys.
97
-
98
- The .s3_sync file takes precedence to the configuration passed in the
99
- activate method.
100
-
101
- A sample `.s3_sync` file is included at the root of this repo.
102
-
103
- > Make sure to add .s3_sync to your ignore list if you choose this approach. Not doing so may expose
104
- > credentials to the world.
105
+ To use these methods, you don't need to specify credentials in your Middleman configuration. The AWS SDK will automatically detect and use them.
105
106
 
106
- #### Through the Command Line
107
+ ##### 2. Environment Variables with Temporary Credentials
107
108
 
108
- The aws credentials can also be passed via a command line options
109
- `--aws_access_key_id` (`-k`) and `--aws_secret_access_key` (`-s`). They should override
110
- any other settings if specified.
109
+ Using environment variables with short-lived credentials from role assumption:
111
110
 
112
- #### Through Environment
111
+ ```bash
112
+ # Obtain temporary credentials via assume-role or similar
113
+ # Then set these environment variables
114
+ export AWS_ACCESS_KEY_ID="temporary-access-key"
115
+ export AWS_SECRET_ACCESS_KEY="temporary-secret-key"
116
+ export AWS_SESSION_TOKEN="temporary-session-token"
117
+ export AWS_BUCKET="your-bucket-name"
118
+ ```
113
119
 
114
- You can also pass the credentials through environment variables. They
115
- map to the following values:
120
+ These environment variables are used when credentials are not otherwise specified:
116
121
 
117
122
  | Setting | Environment Variable |
118
123
  | --------------------- | ---------------------------------- |
119
124
  | aws_access_key_id | ```ENV['AWS_ACCESS_KEY_ID']``` |
120
125
  | aws_secret_access_key | ```ENV['AWS_SECRET_ACCESS_KEY']``` |
126
+ | aws_session_token | ```ENV['AWS_SESSION_TOKEN']``` |
121
127
  | bucket | ```ENV['AWS_BUCKET']``` |
122
128
 
123
- The environment is used when the credentials are not set in the activate
124
- method or passed through the ```.s3_sync``` configuration file.
129
+ #### Alternative Methods (Not Recommended for Production)
125
130
 
126
- #### Through IAM role
131
+ The following methods are less secure and should be avoided in production environments:
127
132
 
128
- Alternatively, if you are running builds on EC2 instance which has approrpiate IAM role, then you don't need to think about specifying credentials at all – they will be pulled from AWS metadata service.
133
+ ##### Through `.s3_sync` File
129
134
 
130
- #### IAM Policy
135
+ You can create a `.s3_sync` at the root of your middleman project.
136
+ The credentials are passed in the YAML format. The keys match the options keys.
137
+
138
+ A sample `.s3_sync` file is included at the root of this repo.
139
+
140
+ > **SECURITY WARNING**: If using this approach, ensure you add `.s3_sync` to your `.gitignore` to prevent
141
+ > accidentally committing credentials to your repository. Consider using this only for local development
142
+ > and only with temporary credentials.
143
+
144
+ ##### Through `config.rb`
145
+
146
+ You can set the AWS credentials in the activation block, but this is strongly discouraged:
147
+
148
+ > **SECURITY WARNING**: This method could lead to credentials being committed to version control,
149
+ > potentially exposing sensitive information. Never use long-lived credentials with this method.
150
+
151
+ ##### Through Command Line
152
+
153
+ Credentials can be passed via command line options, but this may expose them in shell history:
154
+
155
+ > **SECURITY WARNING**: Command line parameters may be visible in process listings or shell history.
156
+ > Consider using environment variables or IAM roles instead.
157
+
158
+ ## CloudFront Invalidation
159
+
160
+ The gem can automatically invalidate CloudFront cache after a successful sync. This ensures that your CloudFront distribution serves the latest content immediately after deployment.
161
+
162
+ ### Configuration
163
+
164
+ ```ruby
165
+ activate :s3_sync do |s3_sync|
166
+ # ... other configuration ...
167
+ s3_sync.cloudfront_distribution_id = 'E1234567890123' # Your CloudFront distribution ID
168
+ s3_sync.cloudfront_invalidate = true # Enable invalidation
169
+ s3_sync.cloudfront_invalidate_all = false # Invalidate only changed files
170
+ end
171
+ ```
172
+
173
+ ### Configuration Options
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>
183
+
184
+ ### Command Line Options
185
+
186
+ You can also control CloudFront invalidation via command line:
187
+
188
+ ```bash
189
+ # Enable CloudFront invalidation for this sync
190
+ middleman s3_sync --cloudfront-invalidate --cloudfront-distribution-id E1234567890123
191
+
192
+ # Invalidate all paths instead of just changed files
193
+ middleman s3_sync --cloudfront-invalidate-all --cloudfront-distribution-id E1234567890123
194
+
195
+ # Wait for invalidation to complete before exiting
196
+ middleman s3_sync --cloudfront-invalidate --cloudfront-wait --cloudfront-distribution-id E1234567890123
197
+
198
+ # Custom batch size for large numbers of files
199
+ middleman s3_sync --cloudfront-invalidate --cloudfront-invalidation-batch-size 500 --cloudfront-distribution-id E1234567890123
200
+
201
+ # Short aliases
202
+ middleman s3_sync -c -d E1234567890123 # Basic invalidation
203
+ middleman s3_sync -a -d E1234567890123 # Invalidate all paths
204
+ middleman s3_sync -c -w -d E1234567890123 # Invalidate and wait
205
+ middleman s3_sync -c -a -w -d E1234567890123 # Invalidate all and wait
206
+ ```
131
207
 
132
- Here's a sample IAM policy that will allow a user to update the site
133
- contained in a bucket named "mysite.com":
208
+ #### Available CloudFront Command Line Options
134
209
 
210
+ | Option | Short | Description |
211
+ | ------ | ----- | ----------- |
212
+ | `--cloudfront-distribution-id` | `-d` | CloudFront distribution ID |
213
+ | `--cloudfront-invalidate` | `-c` | Enable CloudFront invalidation |
214
+ | `--cloudfront-invalidate-all` | `-a` | Invalidate all paths (/*) |
215
+ | `--cloudfront-invalidation-batch-size` | - | Max paths per request (default: 1000) |
216
+ | `--cloudfront-wait` | `-w` | Wait for invalidation to complete |
217
+
218
+ ### How It Works
219
+
220
+ 1. **Smart Invalidation**: By default, only files that were created, updated, or deleted during the sync are invalidated
221
+ 2. **Path Optimization**: Duplicate paths are removed and redundant paths (covered by wildcards) are eliminated
222
+ 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
225
+
226
+ ### IAM Permissions
227
+
228
+ Your AWS credentials need CloudFront permissions in addition to S3:
229
+
230
+ ```json
231
+ {
232
+ "Version": "2012-10-17",
233
+ "Statement": [
234
+ {
235
+ "Effect": "Allow",
236
+ "Action": [
237
+ "cloudfront:CreateInvalidation",
238
+ "cloudfront:GetInvalidation",
239
+ "cloudfront:ListInvalidations"
240
+ ],
241
+ "Resource": "arn:aws:cloudfront::*:distribution/E1234567890123"
242
+ }
243
+ ]
244
+ }
135
245
  ```
246
+
247
+ ### Cost Considerations
248
+
249
+ - CloudFront allows 1,000 free invalidation paths per month
250
+ - Additional invalidations cost $0.005 per path
251
+ - Use `cloudfront_invalidate_all: true` for major updates to minimize costs (counts as 1 path)
252
+ - Consider the trade-off between immediate cache invalidation and cost
253
+
254
+ #### IAM Policy
255
+
256
+ Here's a sample IAM policy with least-privilege permissions that will allow syncing to a bucket named "mysite.com":
257
+
258
+ ```json
136
259
  {
137
260
  "Version": "2012-10-17",
138
261
  "Statement": [
139
262
  {
140
263
  "Effect": "Allow",
141
- "Action": "s3:*",
264
+ "Action": [
265
+ "s3:ListBucket",
266
+ "s3:GetBucketLocation",
267
+ "s3:GetBucketVersioning"
268
+ ],
142
269
  "Resource": "arn:aws:s3:::mysite.com"
143
270
  },
144
271
  {
145
272
  "Effect": "Allow",
146
- "Action": "s3:*",
273
+ "Action": [
274
+ "s3:PutObject",
275
+ "s3:PutObjectAcl",
276
+ "s3:GetObject",
277
+ "s3:GetObjectAcl",
278
+ "s3:DeleteObject",
279
+ "s3:HeadObject"
280
+ ],
147
281
  "Resource": "arn:aws:s3:::mysite.com/*"
148
282
  }
149
283
  ]
150
284
  }
151
285
  ```
152
286
 
153
- This will give full access to both the bucket and it's contents.
287
+ If you're using additional features, you may need these permissions as well:
288
+
289
+ ```json
290
+ {
291
+ "Version": "2012-10-17",
292
+ "Statement": [
293
+ {
294
+ "Effect": "Allow",
295
+ "Action": [
296
+ "s3:PutBucketWebsite",
297
+ "s3:PutBucketVersioning"
298
+ ],
299
+ "Resource": "arn:aws:s3:::mysite.com",
300
+ "Condition": {
301
+ "Bool": {
302
+ "aws:SecureTransport": "true"
303
+ }
304
+ }
305
+ }
306
+ ]
307
+ }
308
+ ```
309
+
310
+ This policy grants only the specific permissions needed:
311
+
312
+ - **For the bucket itself**:
313
+ - `s3:ListBucket`: To list objects in the bucket
314
+ - `s3:GetBucketLocation`: To determine the bucket's region
315
+ - `s3:GetBucketVersioning`: To check versioning status
316
+ - `s3:PutBucketVersioning`: If using the `version_bucket` option
317
+ - `s3:PutBucketWebsite`: If using website configuration (index/error documents)
318
+
319
+ - **For objects in the bucket**:
320
+ - `s3:PutObject`: To create/update objects
321
+ - `s3:PutObjectAcl`: To set ACLs on objects
322
+ - `s3:GetObject`: To retrieve objects for comparison
323
+ - `s3:GetObjectAcl`: To read existing ACLs
324
+ - `s3:DeleteObject`: To delete stray objects (when `delete: true`)
325
+ - `s3:HeadObject`: To retrieve object metadata via HEAD requests
326
+
327
+ The source code shows that middleman-s3_sync uses HEAD requests (`object.head`) to compare resources, checks and sets bucket versioning when configured, and can set website configuration for index and error documents.
328
+
329
+ Note: You can further restrict these permissions by adding conditions or limiting them to specific prefixes if you're only publishing to a subdirectory of the bucket.
154
330
 
155
331
  ## Command Line Usage
156
332
 
@@ -182,17 +358,17 @@ You can specify which environment to run Middleman under using the
182
358
 
183
359
  $ middleman s3_sync --environment=production
184
360
 
185
- You can set up separate sync environments in config.rb like this:
361
+ You can set up separate sync environments in config.rb like this:
186
362
 
187
363
  ```ruby
188
364
  configure :staging do
189
365
  activate :s3_sync do |s3_sync|
190
366
  s3_sync.bucket = '<bucket'
191
- ...
367
+ ...
192
368
  end
193
369
  end
194
370
  ```
195
-
371
+
196
372
  See the Usage section above for all the s3_sync. options to include. Currently, the .s3_sync file does not allow separate environments.
197
373
 
198
374
  #### Dry Run
@@ -291,7 +467,7 @@ The following keys can be set:
291
467
  You can pass the `expires` key to the `caching_policy` and
292
468
  `default_caching_policy` methods if you insist on setting the expires
293
469
  header on a results. You will need to pass it a Time object indicating
294
- when the resourse is set to expire.
470
+ when the resource is set to expire.
295
471
 
296
472
  > Note that the `Cache-Control` header will take precedence over the
297
473
  > `Expires` header if both are present.
@@ -0,0 +1,21 @@
1
+ module Middleman
2
+ module Sitemap
3
+ class Resource
4
+ def redirect?
5
+ false
6
+ end
7
+ end
8
+
9
+ module Extensions
10
+ class RedirectResource < Resource
11
+ def target_url
12
+ @target_url ||= ::Middleman::Util.url_for(@store.app, @request_path, relative: false, find_resource: true)
13
+ end
14
+
15
+ def redirect?
16
+ true
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,192 @@
1
+ require 'aws-sdk-cloudfront'
2
+ require 'securerandom'
3
+
4
+ module Middleman
5
+ module S3Sync
6
+ module CloudFront
7
+ class << self
8
+ include Status
9
+
10
+ def invalidate(invalidation_paths, options)
11
+ return unless should_invalidate?(options)
12
+ return if invalidation_paths.empty? && !options.cloudfront_invalidate_all
13
+
14
+ paths = prepare_invalidation_paths(invalidation_paths, options)
15
+ return if paths.empty?
16
+
17
+ say_status "Invalidating CloudFront distribution #{options.cloudfront_distribution_id}"
18
+
19
+ if options.dry_run
20
+ say_status "#{ANSI.yellow{'DRY RUN:'}} Would invalidate #{paths.length} paths in CloudFront"
21
+ paths.each { |path| say_status " #{path}" } if options.verbose
22
+ return
23
+ end
24
+
25
+ # Split paths into batches to respect CloudFront limits
26
+ batch_size = [options.cloudfront_invalidation_batch_size, 3000].min
27
+ path_batches = paths.each_slice(batch_size).to_a
28
+
29
+ invalidation_ids = []
30
+
31
+ path_batches.each_with_index do |batch, index|
32
+ say_status "Creating invalidation batch #{index + 1}/#{path_batches.length} (#{batch.length} paths)"
33
+
34
+ invalidation_id = create_invalidation(batch, options)
35
+ invalidation_ids << invalidation_id if invalidation_id
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
39
+ end
40
+
41
+ if invalidation_ids.any?
42
+ say_status "CloudFront invalidation(s) created: #{invalidation_ids.join(', ')}"
43
+
44
+ if options.cloudfront_wait
45
+ say_status "Waiting for CloudFront invalidation(s) to complete..."
46
+ wait_for_invalidations(invalidation_ids, options)
47
+ say_status "CloudFront invalidation(s) completed successfully"
48
+ else
49
+ say_status "Invalidations may take 10-15 minutes to complete"
50
+ end
51
+ end
52
+
53
+ invalidation_ids
54
+ rescue Aws::CloudFront::Errors::ServiceError => e
55
+ say_status "#{ANSI.red{'CloudFront invalidation failed:'}} #{e.message}"
56
+ raise e unless options.verbose # Re-raise unless we're being verbose
57
+ end
58
+
59
+ private
60
+
61
+ def should_invalidate?(options)
62
+ return false unless options.cloudfront_invalidate
63
+
64
+ unless options.cloudfront_distribution_id
65
+ say_status "#{ANSI.yellow{'CloudFront invalidation skipped:'}} no distribution ID provided"
66
+ return false
67
+ end
68
+
69
+ true
70
+ end
71
+
72
+ def prepare_invalidation_paths(invalidation_paths, options)
73
+ if options.cloudfront_invalidate_all
74
+ return ['/*']
75
+ end
76
+
77
+ # Normalize paths for CloudFront
78
+ paths = invalidation_paths.map do |path|
79
+ # Ensure path starts with /
80
+ normalized_path = path.start_with?('/') ? path : "/#{path}"
81
+
82
+ # Remove any double slashes
83
+ normalized_path.gsub(/\/+/, '/')
84
+ end.uniq.sort
85
+
86
+ # Remove any paths that would be covered by a wildcard
87
+ if paths.include?('/*')
88
+ paths = ['/*']
89
+ else
90
+ # Remove redundant paths (e.g., if we have /path/* and /path/file.html)
91
+ paths = remove_redundant_paths(paths)
92
+ end
93
+
94
+ say_status "Prepared #{paths.length} paths for CloudFront invalidation" if options.verbose
95
+
96
+ paths
97
+ end
98
+
99
+ def remove_redundant_paths(paths)
100
+ # Sort paths to ensure wildcards come before specific files
101
+ sorted_paths = paths.sort
102
+ result = []
103
+
104
+ sorted_paths.each do |path|
105
+ # Check if this path is already covered by a wildcard we've added
106
+ is_redundant = result.any? do |existing_path|
107
+ if existing_path.end_with?('/*')
108
+ # Check if current path is under this wildcard
109
+ wildcard_prefix = existing_path[0..-3] # Remove /*
110
+ path.start_with?(wildcard_prefix + '/')
111
+ else
112
+ false
113
+ end
114
+ end
115
+
116
+ result << path unless is_redundant
117
+ end
118
+
119
+ result
120
+ end
121
+
122
+ def create_invalidation(paths, options)
123
+ caller_reference = "middleman-s3_sync-#{Time.now.to_i}-#{SecureRandom.hex(4)}"
124
+
125
+ response = cloudfront_client(options).create_invalidation({
126
+ distribution_id: options.cloudfront_distribution_id,
127
+ invalidation_batch: {
128
+ paths: {
129
+ quantity: paths.length,
130
+ items: paths
131
+ },
132
+ caller_reference: caller_reference
133
+ }
134
+ })
135
+
136
+ 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
+ end
143
+
144
+ def cloudfront_client(options)
145
+ client_options = {
146
+ region: 'us-east-1' # CloudFront is always in us-east-1
147
+ }
148
+
149
+ # Use the same credentials as S3 if available
150
+ if options.aws_access_key_id && options.aws_secret_access_key
151
+ client_options.merge!({
152
+ access_key_id: options.aws_access_key_id,
153
+ secret_access_key: options.aws_secret_access_key
154
+ })
155
+
156
+ # If using an assumed role
157
+ client_options.merge!({
158
+ session_token: options.aws_session_token
159
+ }) if options.aws_session_token
160
+ end
161
+
162
+ Aws::CloudFront::Client.new(client_options)
163
+ end
164
+
165
+ def wait_for_invalidations(invalidation_ids, options)
166
+ invalidation_ids.each do |invalidation_id|
167
+ say_status "Waiting for invalidation #{invalidation_id}..."
168
+
169
+ client = cloudfront_client(options)
170
+ client.wait_until(:invalidation_completed,
171
+ distribution_id: options.cloudfront_distribution_id,
172
+ id: invalidation_id
173
+ ) do |waiter|
174
+ waiter.max_attempts = 30 # Wait up to 30 minutes (30 * 60s checks)
175
+ waiter.delay = 60 # Check every 60 seconds
176
+
177
+ waiter.before_attempt do |attempt|
178
+ say_status "Checking invalidation status (attempt #{attempt}/30)..." if options.verbose
179
+ end
180
+ end
181
+ end
182
+ rescue Aws::Waiters::Errors::WaiterFailed => e
183
+ say_status "#{ANSI.yellow{'Warning:'}} CloudFront invalidation wait timed out: #{e.message}"
184
+ say_status "Invalidation is still in progress but sync will continue"
185
+ rescue Aws::CloudFront::Errors::ServiceError => e
186
+ say_status "#{ANSI.yellow{'Warning:'}} CloudFront invalidation wait failed: #{e.message}"
187
+ say_status "Invalidation may still be in progress"
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -6,6 +6,7 @@ module Middleman
6
6
  :http_prefix,
7
7
  :acl,
8
8
  :bucket,
9
+ :endpoint,
9
10
  :region,
10
11
  :aws_access_key_id,
11
12
  :aws_secret_access_key,
@@ -22,6 +23,7 @@ module Middleman
22
23
  :dry_run,
23
24
  :verbose,
24
25
  :content_types,
26
+ :ignore_paths,
25
27
  :index_document,
26
28
  :error_document
27
29
  ]
@@ -67,6 +69,10 @@ module Middleman
67
69
  (@path_style.nil? ? true : @path_style)
68
70
  end
69
71
 
72
+ def ignore_paths
73
+ @ignore_paths.nil? ? [] : @ignore_paths
74
+ end
75
+
70
76
  def prefix=(prefix)
71
77
  http_prefix = @http_prefix ? @http_prefix.sub(%r{^/}, "") : ""
72
78
  if http_prefix.split("/").first == prefix