middleman-s3_sync 4.6.1 → 4.6.3

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: ad34e26fb6ea816a0426ef33e543c4eabc3c5323151f1c40512a95183ae6ed7e
4
- data.tar.gz: a5b875195ed49d7b311eb87a6672a67aba69773e9d85f50fc1022c185238cf41
3
+ metadata.gz: 575e3838865eccc6e585b35829b3891f2a86a77502df0da9e411634ad40312a1
4
+ data.tar.gz: bf897e1d7725bf536759469ef486dc81e51410c67f20b801322a1ad2403a8445
5
5
  SHA512:
6
- metadata.gz: c8f1e1678fd3e45350cdc4efefdd3b2520ad90abd075478dbfcfabb85d8c6294aef1c78acffc7451a9f19cb69b890fd90f6049f35a3000960c87906c7188059f
7
- data.tar.gz: 8b062b097077d16019a1aef109338ab3603941dc4a57d06ea0da23374fc5a947ac0f93b0bbedb08eac1f87e3fd1df38fd7db5f3c116d6a04f4c8bf7f79ee1c39
6
+ metadata.gz: 1ef461a263b9ec005dcca856b34d3b02509550d2ba6d94904920bdf448268aacc547cbdf412406616f4ff409ce70e425ad013cd3e5e0de8a75315c1134d518e9
7
+ data.tar.gz: a00ae49ff314f27e8a3d6878ae00709a67c5759563675b64d72fdc5a61bdc057a19191ab04dea2795f80cfd75c86c00d0e99d707be382227c6c6eeabefeff2c6
data/.envrc.backup 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/.mise.toml ADDED
@@ -0,0 +1,17 @@
1
+ [tools]
2
+ ruby = "3.4"
3
+
4
+ [env]
5
+ _.file = ".env"
6
+ _.path = ["./bin"]
7
+
8
+ [tasks.binstubs]
9
+ description = "Generate binstubs for all gems"
10
+ run = "bundle binstubs --all"
11
+
12
+ [tasks.setup]
13
+ description = "Setup project dependencies and binstubs"
14
+ run = [
15
+ "bundle install",
16
+ "mise run binstubs"
17
+ ]
data/Changelog.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  The gem that tries really hard not to push files to S3.
4
4
 
5
+ ## v4.6.3
6
+ * Restrict incompatible map 8.x installation
7
+
8
+ ## v4.6.2
9
+
10
+ * Fix AWS SDK parameter format issues from Fog migration
11
+ * Fix website configuration parameters to use symbol keys instead of strings
12
+ * Fix S3 object metadata parameter format to use correct key suffixes
13
+ * Remove obsolete Fog-style constants (CONTENT_MD5_KEY, REDIRECT_KEY)
14
+ * Add comprehensive test suite for AWS SDK parameter validation (18 new tests)
15
+ * Improve compatibility with AWS SDK v3 to prevent API errors
16
+
5
17
  ## v4.6.1
6
18
 
7
19
  * Add CloudFront rate limit handling with exponential backoff retry logic
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).
@@ -10,10 +10,10 @@ module Middleman
10
10
  :region,
11
11
  :aws_access_key_id,
12
12
  :aws_secret_access_key,
13
+ :aws_session_token,
13
14
  :after_build,
14
15
  :delete,
15
16
  :encryption,
16
- :existing_remote_file,
17
17
  :build_dir,
18
18
  :force,
19
19
  :prefer_gzip,
@@ -25,12 +25,35 @@ module Middleman
25
25
  :content_types,
26
26
  :ignore_paths,
27
27
  :index_document,
28
- :error_document
28
+ :error_document,
29
+ :cloudfront_distribution_id,
30
+ :cloudfront_invalidate,
31
+ :cloudfront_invalidate_all,
32
+ :cloudfront_invalidation_batch_size,
33
+ :cloudfront_invalidation_max_retries,
34
+ :cloudfront_invalidation_batch_delay,
35
+ :cloudfront_wait
29
36
  ]
30
37
  attr_accessor *OPTIONS
31
38
 
32
39
  def acl
33
- @acl || 'public-read'
40
+ # If @acl is explicitly set to empty string or false, return nil (for buckets with ACLs disabled)
41
+ # If @acl is nil and was never set, return default 'public-read'
42
+ # Otherwise return the set value
43
+ return nil if @acl == '' || @acl == false
44
+ @acl_explicitly_set ? @acl : (@acl || 'public-read')
45
+ end
46
+
47
+ def acl=(value)
48
+ @acl_explicitly_set = true
49
+ @acl = value
50
+ end
51
+
52
+ def acl_enabled?
53
+ # ACLs are disabled if explicitly set to nil, empty string, or false
54
+ return false if @acl_explicitly_set && (@acl.nil? || @acl == '' || @acl == false)
55
+ # Otherwise ACLs are enabled (using default or explicit value)
56
+ true
34
57
  end
35
58
 
36
59
  def aws_access_key_id=(aws_access_key_id)
@@ -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
 
@@ -57,10 +55,11 @@ module Middleman
57
55
  def to_h
58
56
  attributes = {
59
57
  :key => key,
60
- :acl => options.acl,
61
58
  :content_type => content_type,
62
- CONTENT_MD5_KEY => local_content_md5
59
+ 'content-md5' => local_content_md5
63
60
  }
61
+ # Only add ACL if enabled (not for buckets with ACLs disabled)
62
+ attributes[:acl] = options.acl if options.acl_enabled?
64
63
 
65
64
  if caching_policy
66
65
  attributes[:cache_control] = caching_policy.cache_control
@@ -80,7 +79,7 @@ module Middleman
80
79
  end
81
80
 
82
81
  if redirect?
83
- attributes[REDIRECT_KEY] = redirect_url
82
+ attributes['website-redirect-location'] = redirect_url
84
83
  end
85
84
 
86
85
  attributes
@@ -117,40 +116,22 @@ module Middleman
117
116
 
118
117
  def upload!
119
118
  object = bucket.object(remote_path.sub(/^\//, ''))
120
- upload_options = {
121
- body: local_content,
122
- content_type: content_type,
123
- acl: options.acl
124
- }
125
-
126
- # Add metadata if present
127
- if local_content_md5
128
- upload_options[:metadata] = { CONTENT_MD5_KEY => local_content_md5 }
129
- end
130
-
131
- # Add redirect if present
132
- upload_options[:website_redirect_location] = redirect_url if redirect?
133
-
134
- # Add content encoding if present
135
- upload_options[:content_encoding] = "gzip" if options.prefer_gzip && gzipped
136
-
137
- # Add cache control and expires if present
138
- if caching_policy
139
- upload_options[:cache_control] = caching_policy.cache_control
140
- upload_options[:expires] = caching_policy.expires
141
- end
142
-
143
- # Add storage class if needed
144
- if options.reduced_redundancy_storage
145
- upload_options[:storage_class] = 'REDUCED_REDUNDANCY'
146
- end
147
-
148
- # Add encryption if needed
149
- if options.encryption
150
- upload_options[:server_side_encryption] = 'AES256'
119
+ upload_options = build_upload_options
120
+
121
+ begin
122
+ object.put(upload_options)
123
+ rescue Aws::S3::Errors::AccessControlListNotSupported => e
124
+ # Bucket has ACLs disabled - retry without ACL
125
+ if upload_options.key?(:acl)
126
+ say_status "#{ANSI.yellow{"Note"}} Bucket does not support ACLs, retrying without ACL parameter"
127
+ # Automatically disable ACLs for this bucket going forward
128
+ options.acl = ''
129
+ upload_options.delete(:acl)
130
+ retry
131
+ else
132
+ raise e
133
+ end
151
134
  end
152
-
153
- object.put(upload_options)
154
135
  end
155
136
 
156
137
  def ignore!
@@ -292,7 +273,7 @@ module Middleman
292
273
 
293
274
  def remote_content_md5
294
275
  if full_s3_resource && full_s3_resource.metadata
295
- full_s3_resource.metadata[CONTENT_MD5_KEY]
276
+ full_s3_resource.metadata['content-md5']
296
277
  end
297
278
  end
298
279
 
@@ -332,6 +313,45 @@ module Middleman
332
313
  end
333
314
 
334
315
  protected
316
+
317
+ def build_upload_options
318
+ upload_options = {
319
+ body: local_content,
320
+ content_type: content_type
321
+ }
322
+ # Only add ACL if enabled (not for buckets with ACLs disabled)
323
+ upload_options[:acl] = options.acl if options.acl_enabled?
324
+
325
+ # Add metadata if present
326
+ if local_content_md5
327
+ upload_options[:metadata] = { 'content-md5' => local_content_md5 }
328
+ end
329
+
330
+ # Add redirect if present
331
+ upload_options[:website_redirect_location] = redirect_url if redirect?
332
+
333
+ # Add content encoding if present
334
+ upload_options[:content_encoding] = "gzip" if options.prefer_gzip && gzipped
335
+
336
+ # Add cache control and expires if present
337
+ if caching_policy
338
+ upload_options[:cache_control] = caching_policy.cache_control
339
+ upload_options[:expires] = caching_policy.expires
340
+ end
341
+
342
+ # Add storage class if needed
343
+ if options.reduced_redundancy_storage
344
+ upload_options[:storage_class] = 'REDUCED_REDUNDANCY'
345
+ end
346
+
347
+ # Add encryption if needed
348
+ if options.encryption
349
+ upload_options[:server_side_encryption] = 'AES256'
350
+ end
351
+
352
+ upload_options
353
+ end
354
+
335
355
  def bucket
336
356
  Middleman::S3Sync.bucket
337
357
  end
@@ -1,5 +1,5 @@
1
1
  module Middleman
2
2
  module S3Sync
3
- VERSION = "4.6.1"
3
+ VERSION = "4.6.3"
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
 
@@ -77,7 +77,28 @@ module Middleman
77
77
  end
78
78
 
79
79
  def s3_sync_options
80
- options
80
+ self
81
+ end
82
+
83
+ def acl_enabled?
84
+ # ACLs are disabled if acl is explicitly set to nil, empty string, or false
85
+ acl_value = options.acl
86
+ return false if acl_value.nil? || acl_value == '' || acl_value == false
87
+ # Otherwise ACLs are enabled (using default or explicit value)
88
+ true
89
+ end
90
+
91
+ # Delegate option readers to the options object
92
+ def method_missing(method, *args, &block)
93
+ if options.respond_to?(method)
94
+ options.send(method, *args, &block)
95
+ else
96
+ super
97
+ end
98
+ end
99
+
100
+ def respond_to_missing?(method, include_private = false)
101
+ options.respond_to?(method) || super
81
102
  end
82
103
 
83
104
  # Read config options from an IO stream and set them on `self`. Defaults
@@ -20,15 +20,13 @@ Gem::Specification.new do |gem|
20
20
 
21
21
  gem.add_runtime_dependency 'middleman-core'
22
22
  gem.add_runtime_dependency 'middleman-cli'
23
- gem.add_runtime_dependency 'unf'
24
- gem.add_runtime_dependency 'aws-sdk-s3'
23
+ gem.add_runtime_dependency 'aws-sdk-s3', '>= 1.187.0'
25
24
  gem.add_runtime_dependency 'aws-sdk-cloudfront'
26
- gem.add_runtime_dependency 'map'
25
+ gem.add_runtime_dependency 'map', '6.6.0'
27
26
  gem.add_runtime_dependency 'parallel'
28
27
  gem.add_runtime_dependency 'ruby-progressbar'
29
28
  gem.add_runtime_dependency 'ansi', '~> 1.5.0'
30
29
  gem.add_runtime_dependency 'mime-types', '~> 3.1'
31
- gem.add_runtime_dependency 'base64'
32
30
  gem.add_runtime_dependency 'nokogiri', '>= 1.18.4'
33
31
 
34
32
  gem.add_development_dependency 'rake'