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 +4 -4
- data/.envrc.backup +5 -0
- data/.gitignore +1 -0
- data/.mise.toml +17 -0
- data/Changelog.md +12 -0
- data/WARP.md +112 -0
- data/lib/middleman/s3_sync/options.rb +26 -3
- data/lib/middleman/s3_sync/resource.rb +59 -39
- data/lib/middleman/s3_sync/version.rb +1 -1
- data/lib/middleman/s3_sync.rb +3 -3
- data/lib/middleman-s3_sync/extension.rb +22 -1
- data/middleman-s3_sync.gemspec +2 -4
- data/spec/aws_sdk_parameters_spec.rb +525 -0
- data/spec/extension_spec.rb +114 -0
- data/spec/options_spec.rb +272 -0
- metadata +17 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 575e3838865eccc6e585b35829b3891f2a86a77502df0da9e411634ad40312a1
|
|
4
|
+
data.tar.gz: bf897e1d7725bf536759469ef486dc81e51410c67f20b801322a1ad2403a8445
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ef461a263b9ec005dcca856b34d3b02509550d2ba6d94904920bdf448268aacc547cbdf412406616f4ff409ce70e425ad013cd3e5e0de8a75315c1134d518e9
|
|
7
|
+
data.tar.gz: a00ae49ff314f27e8a3d6878ae00709a67c5759563675b64d72fdc5a61bdc057a19191ab04dea2795f80cfd75c86c00d0e99d707be382227c6c6eeabefeff2c6
|
data/.envrc.backup
ADDED
data/.gitignore
CHANGED
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
|
|
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
|
-
|
|
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[
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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[
|
|
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
|
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
|
|
|
@@ -77,7 +77,28 @@ module Middleman
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def s3_sync_options
|
|
80
|
-
|
|
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
|
data/middleman-s3_sync.gemspec
CHANGED
|
@@ -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 '
|
|
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'
|