middleman-s3_sync 4.0.3 → 4.5.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: 0e217a8648de15ba3454749ebf5373e9e2bf31591078be5ae0f5128aeac21d79
4
+ data.tar.gz: 4f82b54882d96b1ece6585e2075a19d8ef54af78fb8aaec42c07ca14cf41fc7d
5
5
  SHA512:
6
- metadata.gz: edbf4fb9046f29261590403220112597f01221350ac223b815856d7a89f833969fbf2a9eb9cb430a9241ffc37603e8baa049baa6ddc64ac1f5b62c6205773453
7
- data.tar.gz: 370d1524220e776febbce93868341fc2d381163768dbfb7b698c64e802625f495aefa0ff833735c270362d9c16f0d812523c517ac0ea05c71c7551e3c8b93efe
6
+ metadata.gz: 2640264fb1f0d4c2be50526d966842176ab9997537a4d2f271b0e1e0e29e4398da44f2c66969b08d9f9f4ddeaeef2014a2b2afe3eb68c95986725aa399524f3c
7
+ data.tar.gz: b9fa2b48b0f63638627750a532e7c7f1fd94a15cf1af4bd9a9826a24021b9fce288d83c9e5d240b4483a0e0488f57b0a7726ede24af9a861d922095bd26eabda
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .aider*
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
@@ -79,7 +79,8 @@ The following defaults apply to the configuration items:
79
79
 
80
80
  ## Setting AWS Credentials
81
81
 
82
- There are several ways to provide the AWS credentials for s3_sync:
82
+ There are several ways to provide the AWS credentials for s3_sync. I strongly recommend using some form of federation to assume a role with permissions to publish to your
83
+ S3 bucket. However, you can still use the following methods::We
83
84
 
84
85
  #### Through `config.rb`
85
86
 
@@ -118,6 +119,7 @@ map to the following values:
118
119
  | --------------------- | ---------------------------------- |
119
120
  | aws_access_key_id | ```ENV['AWS_ACCESS_KEY_ID']``` |
120
121
  | aws_secret_access_key | ```ENV['AWS_SECRET_ACCESS_KEY']``` |
122
+ | aws_session_token | ```ENV['AWS_SESSION_TOKEN']``` |
121
123
  | bucket | ```ENV['AWS_BUCKET']``` |
122
124
 
123
125
  The environment is used when the credentials are not set in the activate
@@ -182,17 +184,17 @@ You can specify which environment to run Middleman under using the
182
184
 
183
185
  $ middleman s3_sync --environment=production
184
186
 
185
- You can set up separate sync environments in config.rb like this:
187
+ You can set up separate sync environments in config.rb like this:
186
188
 
187
189
  ```ruby
188
190
  configure :staging do
189
191
  activate :s3_sync do |s3_sync|
190
192
  s3_sync.bucket = '<bucket'
191
- ...
193
+ ...
192
194
  end
193
195
  end
194
196
  ```
195
-
197
+
196
198
  See the Usage section above for all the s3_sync. options to include. Currently, the .s3_sync file does not allow separate environments.
197
199
 
198
200
  #### Dry Run
@@ -291,7 +293,7 @@ The following keys can be set:
291
293
  You can pass the `expires` key to the `caching_policy` and
292
294
  `default_caching_policy` methods if you insist on setting the expires
293
295
  header on a results. You will need to pass it a Time object indicating
294
- when the resourse is set to expire.
296
+ when the resource is set to expire.
295
297
 
296
298
  > Note that the `Cache-Control` header will take precedence over the
297
299
  > `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
@@ -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
@@ -4,12 +4,19 @@ module Middleman
4
4
  attr_accessor :path, :resource, :partial_s3_resource, :full_s3_resource, :content_type, :gzipped, :options
5
5
 
6
6
  CONTENT_MD5_KEY = 'x-amz-meta-content-md5'
7
+ REDIRECT_KEY = 'x-amz-website-redirect-location'
7
8
 
8
9
  include Status
9
10
 
10
11
  def initialize(resource, partial_s3_resource)
11
12
  @resource = resource
12
- @path = resource ? resource.destination_path : partial_s3_resource.key
13
+ @path = if resource
14
+ resource.destination_path.sub(/^\//, '')
15
+ elsif partial_s3_resource&.key
16
+ partial_s3_resource.key.sub(/^\//, '')
17
+ else
18
+ ''
19
+ end
13
20
  @partial_s3_resource = partial_s3_resource
14
21
  end
15
22
 
@@ -19,13 +26,33 @@ module Middleman
19
26
 
20
27
  # S3 resource as returned by a HEAD request
21
28
  def full_s3_resource
22
- @full_s3_resource ||= bucket.files.head(remote_path)
29
+ @full_s3_resource ||= begin
30
+ bucket.object(remote_path.sub(/^\//, '')).head
31
+ rescue Aws::S3::Errors::NotFound
32
+ nil
33
+ end
23
34
  end
24
35
 
25
36
  def remote_path
26
- s3_resource ? s3_resource.key : "#{options.prefix}#{path}"
37
+ if s3_resource
38
+ if s3_resource.respond_to?(:key)
39
+ s3_resource.key.sub(/^\//, '')
40
+ else
41
+ # For HeadObjectOutput objects which don't have key method
42
+ options.prefix ? normalize_path(options.prefix, path) : path.sub(/^\//, '')
43
+ end
44
+ else
45
+ options.prefix ? normalize_path(options.prefix, path) : path.sub(/^\//, '')
46
+ end.sub(/^\//, '') # Ensure no leading slash
27
47
  end
28
48
  alias :key :remote_path
49
+
50
+ def normalize_path(prefix, path)
51
+ # Remove any trailing slash from prefix and leading slash from path
52
+ prefix = prefix.chomp('/')
53
+ path = path.sub(/^\//, '')
54
+ "#{prefix}/#{path}"
55
+ end
29
56
 
30
57
  def to_h
31
58
  attributes = {
@@ -52,18 +79,19 @@ module Middleman
52
79
  attributes[:encryption] = 'AES256'
53
80
  end
54
81
 
82
+ if redirect?
83
+ attributes[REDIRECT_KEY] = redirect_url
84
+ end
85
+
55
86
  attributes
56
87
  end
57
88
  alias :attributes :to_h
58
89
 
59
90
  def update!
60
- local_content { |body|
61
- say_status "#{ANSI.blue{"Updating"}} #{remote_path}#{ gzipped ? ANSI.white {' (gzipped)'} : ''}"
62
- s3_resource.merge_attributes(to_h)
63
- s3_resource.body = body
64
-
65
- s3_resource.save unless options.dry_run
66
- }
91
+ say_status "#{ANSI.blue{"Updating"}} #{remote_path}#{ gzipped ? ANSI.white {' (gzipped)'} : ''}"
92
+ unless options.dry_run
93
+ upload!
94
+ end
67
95
  end
68
96
 
69
97
  def local_path
@@ -77,14 +105,52 @@ module Middleman
77
105
 
78
106
  def destroy!
79
107
  say_status "#{ANSI.red{"Deleting"}} #{remote_path}"
80
- bucket.files.destroy remote_path unless options.dry_run
108
+ bucket.object(remote_path.sub(/^\//, '')).delete unless options.dry_run
81
109
  end
82
110
 
83
111
  def create!
84
112
  say_status "#{ANSI.green{"Creating"}} #{remote_path}#{ gzipped ? ANSI.white {' (gzipped)'} : ''}"
85
- local_content { |body|
86
- bucket.files.create(to_h.merge(body: body)) unless options.dry_run
113
+ unless options.dry_run
114
+ upload!
115
+ end
116
+ end
117
+
118
+ def upload!
119
+ object = bucket.object(remote_path.sub(/^\//, ''))
120
+ upload_options = {
121
+ body: local_content,
122
+ content_type: content_type,
123
+ acl: options.acl
87
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'
151
+ end
152
+
153
+ object.put(upload_options)
88
154
  end
89
155
 
90
156
  def ignore!
@@ -122,12 +188,18 @@ module Middleman
122
188
  status == :ignored || status == :alternate_encoding
123
189
  end
124
190
 
125
- def local_content(&block)
126
- File.open(local_path, &block)
191
+ def local_content
192
+ if block_given?
193
+ File.open(local_path) { |f| yield f.read }
194
+ else
195
+ File.read(local_path)
196
+ end
127
197
  end
128
198
 
129
199
  def status
130
- @status ||= if directory?
200
+ @status ||= if shunned?
201
+ :ignored
202
+ elsif directory?
131
203
  if remote?
132
204
  :deleted
133
205
  else
@@ -136,7 +208,7 @@ module Middleman
136
208
  elsif local? && remote?
137
209
  if options.force
138
210
  :updated
139
- elsif not caching_policy_match?
211
+ elsif not metadata_match?
140
212
  :updated
141
213
  elsif local_object_md5 == remote_object_md5
142
214
  :identical
@@ -170,11 +242,36 @@ module Middleman
170
242
  end
171
243
 
172
244
  def remote?
173
- !!s3_resource
245
+ !full_s3_resource.nil?
174
246
  end
175
247
 
176
248
  def redirect?
177
- full_s3_resource && full_s3_resource.metadata.has_key?('x-amz-website-redirect-location')
249
+ (resource && resource.respond_to?(:redirect?) && resource.redirect?) ||
250
+ (full_s3_resource && full_s3_resource.respond_to?(:website_redirect_location) && full_s3_resource.website_redirect_location)
251
+ end
252
+
253
+ def metadata_match?
254
+ redirect_match? && caching_policy_match?
255
+ end
256
+
257
+ def redirect_match?
258
+ if redirect?
259
+ redirect_url == remote_redirect_url
260
+ else
261
+ true
262
+ end
263
+ end
264
+
265
+ def shunned?
266
+ !!path[Regexp.union(options.ignore_paths)]
267
+ end
268
+
269
+ def remote_redirect_url
270
+ full_s3_resource&.website_redirect_location
271
+ end
272
+
273
+ def redirect_url
274
+ resource.respond_to?(:target_url) ? resource.target_url : nil
178
275
  end
179
276
 
180
277
  def directory?
@@ -186,7 +283,7 @@ module Middleman
186
283
  end
187
284
 
188
285
  def remote_object_md5
189
- s3_resource.etag
286
+ s3_resource.etag.gsub(/"/, '') if s3_resource.etag
190
287
  end
191
288
 
192
289
  def encoding_match?
@@ -194,7 +291,9 @@ module Middleman
194
291
  end
195
292
 
196
293
  def remote_content_md5
197
- full_s3_resource.metadata[CONTENT_MD5_KEY]
294
+ if full_s3_resource && full_s3_resource.metadata
295
+ full_s3_resource.metadata[CONTENT_MD5_KEY]
296
+ end
198
297
  end
199
298
 
200
299
  def local_object_md5
@@ -202,7 +301,13 @@ module Middleman
202
301
  end
203
302
 
204
303
  def local_content_md5
205
- @local_content_md5 ||= Digest::MD5.hexdigest(File.read(original_path))
304
+ @local_content_md5 ||= begin
305
+ if File.exist?(original_path)
306
+ Digest::MD5.hexdigest(File.read(original_path))
307
+ else
308
+ nil
309
+ end
310
+ end
206
311
  end
207
312
 
208
313
  def original_path
@@ -211,7 +316,7 @@ module Middleman
211
316
 
212
317
  def content_type
213
318
  @content_type ||= Middleman::S3Sync.content_types[local_path]
214
- @content_type ||= !resource.nil? ? resource.content_type : nil
319
+ @content_type ||= !resource.nil? && resource.respond_to?(:content_type) ? resource.content_type : nil
215
320
  end
216
321
 
217
322
  def caching_policy
@@ -219,7 +324,7 @@ module Middleman
219
324
  end
220
325
 
221
326
  def caching_policy_match?
222
- if (caching_policy)
327
+ if caching_policy && full_s3_resource && full_s3_resource.respond_to?(:cache_control)
223
328
  caching_policy.cache_control == full_s3_resource.cache_control
224
329
  else
225
330
  true
@@ -1,5 +1,5 @@
1
1
  module Middleman
2
2
  module S3Sync
3
- VERSION = "4.0.3"
3
+ VERSION = "4.5.0"
4
4
  end
5
5
  end
@@ -1,5 +1,4 @@
1
- require 'fog/aws'
2
- require 'fog/aws/storage'
1
+ require 'aws-sdk-s3'
3
2
  require 'digest/md5'
4
3
  require 'middleman/s3_sync/version'
5
4
  require 'middleman/s3_sync/options'
@@ -7,6 +6,7 @@ require 'middleman/s3_sync/caching_policy'
7
6
  require 'middleman/s3_sync/status'
8
7
  require 'middleman/s3_sync/resource'
9
8
  require 'middleman-s3_sync/extension'
9
+ require 'middleman/redirect'
10
10
  require 'parallel'
11
11
  require 'ruby-progressbar'
12
12
  require 'thread'
@@ -24,6 +24,8 @@ module Middleman
24
24
  attr_accessor :mm_resources
25
25
  attr_reader :app
26
26
 
27
+ THREADS_COUNT = 8
28
+
27
29
  def sync()
28
30
  @app ||= ::Middleman::Application.new
29
31
 
@@ -48,8 +50,8 @@ module Middleman
48
50
  def bucket
49
51
  @@bucket_lock.synchronize do
50
52
  @bucket ||= begin
51
- bucket = connection.directories.get(s3_sync_options.bucket, :prefix => s3_sync_options.prefix)
52
- raise "Bucket #{s3_sync_options.bucket} doesn't exist!" unless bucket
53
+ bucket = s3_resource.bucket(s3_sync_options.bucket)
54
+ raise "Bucket #{s3_sync_options.bucket} doesn't exist!" unless bucket.exists?
53
55
  bucket
54
56
  end
55
57
  end
@@ -73,7 +75,12 @@ module Middleman
73
75
 
74
76
  protected
75
77
  def update_bucket_versioning
76
- connection.put_bucket_versioning(s3_sync_options.bucket, "Enabled") if s3_sync_options.version_bucket
78
+ s3_client.put_bucket_versioning({
79
+ bucket: s3_sync_options.bucket,
80
+ versioning_configuration: {
81
+ status: "Enabled"
82
+ }
83
+ }) if s3_sync_options.version_bucket
77
84
  end
78
85
 
79
86
  def update_bucket_website
@@ -87,30 +94,47 @@ module Middleman
87
94
 
88
95
  unless opts.empty?
89
96
  say_status "Putting bucket website: #{opts.to_json}"
90
- connection.put_bucket_website(s3_sync_options.bucket, opts)
97
+ s3_client.put_bucket_website({
98
+ bucket: s3_sync_options.bucket,
99
+ website_configuration: opts
100
+ })
91
101
  end
92
102
  end
93
103
 
94
- def connection
95
- connection_options = {
96
- :region => s3_sync_options.region,
97
- :path_style => s3_sync_options.path_style
98
- }
104
+ def s3_client
105
+ @s3_client ||= Aws::S3::Client.new(connection_options)
106
+ end
99
107
 
100
- if s3_sync_options.aws_access_key_id && s3_sync_options.aws_secret_access_key
101
- connection_options.merge!({
102
- :aws_access_key_id => s3_sync_options.aws_access_key_id,
103
- :aws_secret_access_key => s3_sync_options.aws_secret_access_key
104
- })
105
- else
106
- connection_options.merge!({ :use_iam_profile => true })
107
- end
108
+ def s3_resource
109
+ @s3_resource ||= Aws::S3::Resource.new(client: s3_client)
110
+ end
111
+
112
+ def connection_options
113
+ @connection_options ||= begin
114
+ connection_options = {
115
+ endpoint: s3_sync_options.endpoint,
116
+ region: s3_sync_options.region,
117
+ force_path_style: s3_sync_options.path_style
118
+ }
119
+
120
+ if s3_sync_options.aws_access_key_id && s3_sync_options.aws_secret_access_key
121
+ connection_options.merge!({
122
+ access_key_id: s3_sync_options.aws_access_key_id,
123
+ secret_access_key: s3_sync_options.aws_secret_access_key
124
+ })
125
+
126
+ # If using an assumed role
127
+ connection_options.merge!({
128
+ session_token: s3_sync_options.aws_session_token
129
+ }) if s3_sync_options.aws_session_token
130
+ end
108
131
 
109
- @connection ||= Fog::Storage::AWS.new(connection_options)
132
+ connection_options
133
+ end
110
134
  end
111
135
 
112
136
  def remote_resource_for_path(path)
113
- bucket_files.find { |f| f.key == "#{s3_sync_options.prefix}#{path}" }
137
+ bucket_files[path]
114
138
  end
115
139
 
116
140
  def s3_sync_resources
@@ -125,7 +149,7 @@ module Middleman
125
149
 
126
150
  def remote_paths
127
151
  @remote_paths ||= if s3_sync_options.delete
128
- bucket_files.map(&:key)
152
+ bucket_files.keys
129
153
  else
130
154
  []
131
155
  end
@@ -133,42 +157,36 @@ module Middleman
133
157
 
134
158
  def bucket_files
135
159
  @@bucket_files_lock.synchronize do
136
- @bucket_files ||= [].tap { |files|
137
- bucket.files.each { |f|
138
- files << f
139
- }
140
- }
160
+ @bucket_files ||= begin
161
+ files = {}
162
+ bucket.objects.each do |object|
163
+ files[object.key] = object
164
+ end
165
+ files
166
+ end
141
167
  end
142
168
  end
143
169
 
144
170
  def create_resources
145
- files_to_create.each do |r|
146
- r.create!
147
- end
171
+ Parallel.map(files_to_create, in_threads: THREADS_COUNT, &:create!)
148
172
  end
149
173
 
150
174
  def update_resources
151
- files_to_update.each do |r|
152
- r.update!
153
- end
175
+ Parallel.map(files_to_update, in_threads: THREADS_COUNT, &:update!)
154
176
  end
155
177
 
156
178
  def delete_resources
157
- files_to_delete.each do |r|
158
- r.destroy!
159
- end
179
+ Parallel.map(files_to_delete, in_threads: THREADS_COUNT, &:destroy!)
160
180
  end
161
181
 
162
182
  def ignore_resources
163
- files_to_ignore.each do |r|
164
- r.ignore!
165
- end
183
+ Parallel.map(files_to_ignore, in_threads: THREADS_COUNT, &:ignore!)
166
184
  end
167
185
 
168
186
  def work_to_be_done?
169
- Parallel.each(mm_resources, in_threads: 8, progress: "Processing sitemap") { |mm_resource| add_local_resource(mm_resource) }
187
+ Parallel.each(mm_resources, in_threads: THREADS_COUNT, progress: "Processing sitemap") { |mm_resource| add_local_resource(mm_resource) }
170
188
 
171
- Parallel.each(remote_only_paths, in_threads: 8, progress: "Processing remote files") do |remote_path|
189
+ Parallel.each(remote_only_paths, in_threads: THREADS_COUNT, progress: "Processing remote files") do |remote_path|
172
190
  s3_sync_resources[remote_path] ||= S3Sync::Resource.new(nil, remote_resource_for_path(remote_path)).tap(&:status)
173
191
  end
174
192
 
@@ -70,8 +70,10 @@ module Middleman
70
70
  verbose = options[:verbose] ? 0 : 1
71
71
  instrument = options[:instrument]
72
72
 
73
+ mode = options[:build] ? :build : :config
74
+
73
75
  ::Middleman::S3Sync.app = ::Middleman::Application.new do
74
- config[:mode] = :build
76
+ config[:mode] = mode
75
77
  config[:environment] = env
76
78
  ::Middleman::Logger.singleton(verbose, instrument)
77
79
  end
@@ -9,9 +9,11 @@ module Middleman
9
9
  option :http_prefix, nil, 'Path prefix of the resources'
10
10
  option :acl, 'public-read', 'ACL for the resources being pushed to S3'
11
11
  option :bucket, nil, 'The name of the bucket we are pushing to.'
12
+ option :endpoint, nil, 'The name of the endpoint to use - useful when using S3 compatible storage'
12
13
  option :region, 'us-east-1', 'The name of the AWS region hosting the S3 bucket'
13
14
  option :aws_access_key_id, ENV['AWS_ACCESS_KEY_ID'] , 'The AWS access key id'
14
15
  option :aws_secret_access_key, ENV['AWS_SECRET_ACCESS_KEY'], 'The AWS secret access key'
16
+ option :aws_session_token, ENV['AWS_SESSION_TOKEN'] || ENV['AWS_SECURITY_TOKEN'], 'The AWS session token (for assuming roles)'
15
17
  option :after_build, false, 'Whether to synchronize right after the build'
16
18
  option :build_dir, nil, 'Where the built site is stored'
17
19
  option :delete, true, 'Whether to delete resources that do not have a local equivalent'
@@ -26,6 +28,7 @@ module Middleman
26
28
  option :index_document, nil, 'S3 custom index document path'
27
29
  option :error_document, nil, 'S3 custom error document path'
28
30
  option :content_types, {}, 'Custom content types'
31
+ option :ignore_paths, [], 'Paths that should be ignored during sync, strings or regex are allowed'
29
32
 
30
33
  expose_to_config :s3_sync_options, :default_caching_policy, :caching_policy
31
34
 
@@ -40,6 +43,7 @@ module Middleman
40
43
  read_config
41
44
  options.aws_access_key_id ||= ENV['AWS_ACCESS_KEY_ID']
42
45
  options.aws_secret_access_key ||= ENV['AWS_SECRET_ACCESS_KEY']
46
+ options.aws_session_token ||= ENV['AWS_SESSION_TOKEN'] || ENV['AWS_SECURITY_TOKEN']
43
47
  options.bucket ||= ENV['AWS_BUCKET']
44
48
  options.http_prefix = app.http_prefix if app.respond_to? :http_prefix
45
49
  options.build_dir ||= app.build_dir if app.respond_to? :build_dir
@@ -54,8 +58,15 @@ module Middleman
54
58
  ::Middleman::S3Sync.sync() if options.after_build
55
59
  end
56
60
 
57
- def manipulate_resource_list(mm_resources)
58
- ::Middleman::S3Sync.mm_resources = mm_resources
61
+ def manipulate_resource_list(resources)
62
+ ::Middleman::S3Sync.mm_resources = resources.each_with_object([]) do |resource, list|
63
+ next if resource.ignored?
64
+
65
+ list << resource
66
+ list << resource.target_resource if resource.respond_to?(:target_resource)
67
+ end
68
+
69
+ resources
59
70
  end
60
71
 
61
72
  def s3_sync_options
@@ -73,7 +84,7 @@ module Middleman
73
84
  config_file_path = File.join(root_path, ".s3_sync")
74
85
 
75
86
  # skip if config file does not exist
76
- return unless File.exists?(config_file_path)
87
+ return unless File.exist?(config_file_path)
77
88
 
78
89
  io = File.open(config_file_path, "r")
79
90
  end
@@ -1,3 +1,4 @@
1
+ require 'logger'
1
2
  require 'middleman-core'
2
3
  require 'middleman-s3_sync/commands'
3
4
  require 'middleman/s3_sync'
@@ -18,22 +18,25 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_runtime_dependency 'middleman-core', '>= 4.0.0'
21
+ gem.add_runtime_dependency 'middleman-core'
22
22
  gem.add_runtime_dependency 'middleman-cli'
23
23
  gem.add_runtime_dependency 'unf'
24
- gem.add_runtime_dependency 'fog-aws', '>= 0.1.1'
24
+ gem.add_runtime_dependency 'aws-sdk-s3'
25
25
  gem.add_runtime_dependency 'map'
26
26
  gem.add_runtime_dependency 'parallel'
27
27
  gem.add_runtime_dependency 'ruby-progressbar'
28
28
  gem.add_runtime_dependency 'ansi', '~> 1.5.0'
29
+ gem.add_runtime_dependency 'mime-types', '~> 3.1'
30
+ gem.add_runtime_dependency 'base64'
31
+ gem.add_runtime_dependency 'nokogiri', '>= 1.18.4'
29
32
 
30
33
  gem.add_development_dependency 'rake'
31
34
  gem.add_development_dependency 'pry'
32
35
  gem.add_development_dependency 'pry-byebug'
33
- gem.add_development_dependency 'rspec', '>= 3.0.0'
36
+ gem.add_development_dependency 'rspec'
37
+ gem.add_development_dependency 'rspec-support'
34
38
  gem.add_development_dependency 'rspec-its'
35
39
  gem.add_development_dependency 'rspec-mocks'
36
40
  gem.add_development_dependency 'timerizer'
37
- gem.add_development_dependency 'travis'
38
- gem.add_development_dependency 'travis-lint'
41
+ gem.add_development_dependency 'webrick'
39
42
  end
@@ -8,13 +8,39 @@ describe Middleman::S3Sync::Resource do
8
8
 
9
9
  let(:mm_resource) {
10
10
  double(
11
- destination_path: 'path/to/resource.html'
11
+ destination_path: 'path/to/resource.html',
12
+ content_type: 'text/html'
12
13
  )
13
14
  }
15
+
16
+ let(:s3_client) { instance_double(Aws::S3::Client) }
17
+ let(:s3_resource) { instance_double(Aws::S3::Resource) }
18
+ let(:bucket) { instance_double(Aws::S3::Bucket) }
19
+ let(:s3_object) { instance_double(Aws::S3::Object) }
20
+
14
21
  before do
15
22
  Middleman::S3Sync.s3_sync_options = options
23
+
16
24
  options.build_dir = "build"
17
25
  options.prefer_gzip = false
26
+ options.bucket = "test-bucket"
27
+ options.acl = "public-read"
28
+
29
+ allow(Aws::S3::Client).to receive(:new).and_return(s3_client)
30
+ allow(Aws::S3::Resource).to receive(:new).and_return(s3_resource)
31
+ allow(s3_resource).to receive(:bucket).and_return(bucket)
32
+ allow(bucket).to receive(:exists?).and_return(true)
33
+ allow(bucket).to receive(:object) do |path|
34
+ # Ensure path has no leading slash
35
+ path = path.sub(/^\//, '') if path.is_a?(String)
36
+ s3_object
37
+ end
38
+ allow(s3_object).to receive(:head).and_return(nil)
39
+ allow(s3_object).to receive(:put).and_return(true)
40
+ allow(s3_object).to receive(:delete).and_return(true)
41
+
42
+ # Allow Middleman::S3Sync to use our mocked bucket
43
+ allow(Middleman::S3Sync).to receive(:bucket).and_return(bucket)
18
44
  end
19
45
 
20
46
  context "a new resource" do
@@ -23,6 +49,7 @@ describe Middleman::S3Sync::Resource do
23
49
  context "without a prefix" do
24
50
  before do
25
51
  allow(File).to receive(:exist?).with('build/path/to/resource.html').and_return(true)
52
+ allow(File).to receive(:read).with('build/path/to/resource.html').and_return('test content')
26
53
  end
27
54
 
28
55
  its(:status) { is_expected.to eq :new }
@@ -31,11 +58,11 @@ describe Middleman::S3Sync::Resource do
31
58
  expect(resource).not_to be_remote
32
59
  end
33
60
 
34
- it "exits locally" do
61
+ it "exists locally" do
35
62
  expect(resource).to be_local
36
63
  end
37
64
 
38
- its(:path) { is_expected.to eq 'path/to/resource.html'}
65
+ its(:path) { is_expected.to eq 'path/to/resource.html' }
39
66
  its(:local_path) { is_expected.to eq 'build/path/to/resource.html' }
40
67
  its(:remote_path) { is_expected.to eq 'path/to/resource.html' }
41
68
  end
@@ -43,6 +70,7 @@ describe Middleman::S3Sync::Resource do
43
70
  context "with a prefix set" do
44
71
  before do
45
72
  allow(File).to receive(:exist?).with('build/path/to/resource.html').and_return(true)
73
+ allow(File).to receive(:read).with('build/path/to/resource.html').and_return('test content')
46
74
  options.prefix = "bob"
47
75
  end
48
76
 
@@ -62,6 +90,9 @@ describe Middleman::S3Sync::Resource do
62
90
  context "gzipped" do
63
91
  before do
64
92
  allow(File).to receive(:exist?).with('build/path/to/resource.html.gz').and_return(true)
93
+ allow(File).to receive(:read).with('build/path/to/resource.html.gz').and_return('gzipped content')
94
+ allow(File).to receive(:exist?).with('build/path/to/resource.html').and_return(true)
95
+ allow(File).to receive(:read).with('build/path/to/resource.html').and_return('test content')
65
96
  options.prefer_gzip = true
66
97
  end
67
98
 
@@ -82,12 +113,7 @@ describe Middleman::S3Sync::Resource do
82
113
  context "the file does not exist locally" do
83
114
  subject(:resource) { Middleman::S3Sync::Resource.new(nil, remote) }
84
115
 
85
- let(:remote) {
86
- double(
87
- key: 'path/to/resource.html',
88
- metadata: {}
89
- )
90
- }
116
+ let(:remote) { mock_s3_object('path/to/resource.html') }
91
117
 
92
118
  before do
93
119
  resource.full_s3_resource = remote
@@ -103,7 +129,7 @@ describe Middleman::S3Sync::Resource do
103
129
  expect(resource).to be_remote
104
130
  end
105
131
 
106
- it "exits locally" do
132
+ it "does not exist locally" do
107
133
  expect(resource).not_to be_local
108
134
  end
109
135
 
@@ -115,8 +141,9 @@ describe Middleman::S3Sync::Resource do
115
141
  context "with a prefix set" do
116
142
  before do
117
143
  allow(File).to receive(:exist?).with('build/path/to/resource.html').and_return(false)
118
- allow(remote).to receive(:key).and_return('bob/path/to/resource.html')
119
- options.prefix = "bob"
144
+ remote = mock_s3_object('bob/path/to/resource.html')
145
+ resource.full_s3_resource = remote
146
+ options.prefix = "bob/"
120
147
  end
121
148
 
122
149
  its(:status) { is_expected.to eq :deleted }
@@ -145,7 +172,7 @@ describe Middleman::S3Sync::Resource do
145
172
  expect(resource).to be_remote
146
173
  end
147
174
 
148
- it "exists locally" do
175
+ it "does not exist locally" do
149
176
  expect(resource).not_to be_local
150
177
  end
151
178
 
@@ -154,4 +181,42 @@ describe Middleman::S3Sync::Resource do
154
181
  its(:remote_path) { is_expected.to eq 'path/to/resource.html' }
155
182
  end
156
183
  end
184
+
185
+ context 'An ignored resource' do
186
+ context "that is local" do
187
+
188
+ subject(:resource) { Middleman::S3Sync::Resource.new(mm_resource, nil) }
189
+
190
+ let(:mm_resource) {
191
+ double(
192
+ destination_path: 'ignored/path/to/resource.html',
193
+ content_type: 'text/html'
194
+ )
195
+ }
196
+
197
+ before do
198
+ allow(File).to receive(:exist?).with('build/ignored/path/to/resource.html').and_return(true)
199
+ allow(File).to receive(:read).with('build/ignored/path/to/resource.html').and_return('test content')
200
+ options.ignore_paths = [/^ignored/]
201
+ end
202
+
203
+ its(:status) { is_expected.to eq :ignored }
204
+ end
205
+
206
+ context "that is remote" do
207
+
208
+ subject(:resource) { Middleman::S3Sync::Resource.new(nil, remote) }
209
+
210
+ let(:remote) { mock_s3_object('ignored/path/to/resource.html') }
211
+
212
+ before do
213
+ resource.full_s3_resource = remote
214
+ options.ignore_paths = [/^ignored/]
215
+ end
216
+
217
+ its(:status) { is_expected.to eq :ignored }
218
+ end
219
+
220
+ end
221
+
157
222
  end
data/spec/spec_helper.rb CHANGED
@@ -8,6 +8,9 @@
8
8
  require 'middleman-s3_sync'
9
9
  require 'timerizer'
10
10
  require 'rspec/its'
11
+ require 'rspec/support'
12
+ require 'digest/md5'
13
+ require 'cgi'
11
14
 
12
15
  RSpec.configure do |config|
13
16
  config.run_all_when_everything_filtered = true
@@ -20,6 +23,43 @@ RSpec.configure do |config|
20
23
  config.order = 'random'
21
24
 
22
25
  config.before :all do
23
- Fog.mock!
26
+ Aws.config.update(
27
+ region: 'us-east-1',
28
+ credentials: Aws::Credentials.new('access_key_id', 'secret_access_key'),
29
+ stub_responses: true
30
+ )
31
+ end
32
+
33
+ # Helper method to create a mock S3 object
34
+ def mock_s3_object(key, metadata = {})
35
+ # Ensure key has no leading slash to match test expectations
36
+ key = key.sub(/^\//, '')
37
+
38
+ obj = double(
39
+ key: key,
40
+ metadata: metadata,
41
+ etag: "\"#{Digest::MD5.hexdigest('test content')}\"",
42
+ content_encoding: nil,
43
+ cache_control: nil,
44
+ website_redirect_location: nil
45
+ )
46
+
47
+ # Allow head method to return self for testing
48
+ allow(obj).to receive(:head).and_return(obj)
49
+ allow(obj).to receive(:put).and_return(true)
50
+ allow(obj).to receive(:delete).and_return(true)
51
+
52
+ obj
53
+ end
54
+
55
+ # Helper method to create a mock HeadObjectOutput
56
+ def mock_head_object(metadata = {})
57
+ double(
58
+ metadata: metadata,
59
+ etag: "\"#{Digest::MD5.hexdigest('test content')}\"",
60
+ content_encoding: nil,
61
+ cache_control: nil,
62
+ website_redirect_location: nil
63
+ )
24
64
  end
25
65
  end
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: middleman-s3_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.3
4
+ version: 4.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Frederic Jean
8
8
  - Will Koehler
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2016-05-10 00:00:00.000000000 Z
11
+ date: 2025-04-28 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: middleman-core
@@ -17,14 +16,14 @@ dependencies:
17
16
  requirements:
18
17
  - - ">="
19
18
  - !ruby/object:Gem::Version
20
- version: 4.0.0
19
+ version: '0'
21
20
  type: :runtime
22
21
  prerelease: false
23
22
  version_requirements: !ruby/object:Gem::Requirement
24
23
  requirements:
25
24
  - - ">="
26
25
  - !ruby/object:Gem::Version
27
- version: 4.0.0
26
+ version: '0'
28
27
  - !ruby/object:Gem::Dependency
29
28
  name: middleman-cli
30
29
  requirement: !ruby/object:Gem::Requirement
@@ -54,19 +53,19 @@ dependencies:
54
53
  - !ruby/object:Gem::Version
55
54
  version: '0'
56
55
  - !ruby/object:Gem::Dependency
57
- name: fog-aws
56
+ name: aws-sdk-s3
58
57
  requirement: !ruby/object:Gem::Requirement
59
58
  requirements:
60
59
  - - ">="
61
60
  - !ruby/object:Gem::Version
62
- version: 0.1.1
61
+ version: '0'
63
62
  type: :runtime
64
63
  prerelease: false
65
64
  version_requirements: !ruby/object:Gem::Requirement
66
65
  requirements:
67
66
  - - ">="
68
67
  - !ruby/object:Gem::Version
69
- version: 0.1.1
68
+ version: '0'
70
69
  - !ruby/object:Gem::Dependency
71
70
  name: map
72
71
  requirement: !ruby/object:Gem::Requirement
@@ -123,6 +122,48 @@ dependencies:
123
122
  - - "~>"
124
123
  - !ruby/object:Gem::Version
125
124
  version: 1.5.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: mime-types
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.1'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.1'
139
+ - !ruby/object:Gem::Dependency
140
+ name: base64
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: nokogiri
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: 1.18.4
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: 1.18.4
126
167
  - !ruby/object:Gem::Dependency
127
168
  name: rake
128
169
  requirement: !ruby/object:Gem::Requirement
@@ -171,16 +212,16 @@ dependencies:
171
212
  requirements:
172
213
  - - ">="
173
214
  - !ruby/object:Gem::Version
174
- version: 3.0.0
215
+ version: '0'
175
216
  type: :development
176
217
  prerelease: false
177
218
  version_requirements: !ruby/object:Gem::Requirement
178
219
  requirements:
179
220
  - - ">="
180
221
  - !ruby/object:Gem::Version
181
- version: 3.0.0
222
+ version: '0'
182
223
  - !ruby/object:Gem::Dependency
183
- name: rspec-its
224
+ name: rspec-support
184
225
  requirement: !ruby/object:Gem::Requirement
185
226
  requirements:
186
227
  - - ">="
@@ -194,7 +235,7 @@ dependencies:
194
235
  - !ruby/object:Gem::Version
195
236
  version: '0'
196
237
  - !ruby/object:Gem::Dependency
197
- name: rspec-mocks
238
+ name: rspec-its
198
239
  requirement: !ruby/object:Gem::Requirement
199
240
  requirements:
200
241
  - - ">="
@@ -208,7 +249,7 @@ dependencies:
208
249
  - !ruby/object:Gem::Version
209
250
  version: '0'
210
251
  - !ruby/object:Gem::Dependency
211
- name: timerizer
252
+ name: rspec-mocks
212
253
  requirement: !ruby/object:Gem::Requirement
213
254
  requirements:
214
255
  - - ">="
@@ -222,7 +263,7 @@ dependencies:
222
263
  - !ruby/object:Gem::Version
223
264
  version: '0'
224
265
  - !ruby/object:Gem::Dependency
225
- name: travis
266
+ name: timerizer
226
267
  requirement: !ruby/object:Gem::Requirement
227
268
  requirements:
228
269
  - - ">="
@@ -236,7 +277,7 @@ dependencies:
236
277
  - !ruby/object:Gem::Version
237
278
  version: '0'
238
279
  - !ruby/object:Gem::Dependency
239
- name: travis-lint
280
+ name: webrick
240
281
  requirement: !ruby/object:Gem::Requirement
241
282
  requirements:
242
283
  - - ">="
@@ -268,6 +309,7 @@ files:
268
309
  - lib/middleman-s3_sync.rb
269
310
  - lib/middleman-s3_sync/commands.rb
270
311
  - lib/middleman-s3_sync/extension.rb
312
+ - lib/middleman/redirect.rb
271
313
  - lib/middleman/s3_sync.rb
272
314
  - lib/middleman/s3_sync/caching_policy.rb
273
315
  - lib/middleman/s3_sync/options.rb
@@ -283,7 +325,6 @@ homepage: http://github.com/fredjean/middleman-s3_sync
283
325
  licenses:
284
326
  - MIT
285
327
  metadata: {}
286
- post_install_message:
287
328
  rdoc_options: []
288
329
  require_paths:
289
330
  - lib
@@ -298,9 +339,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
298
339
  - !ruby/object:Gem::Version
299
340
  version: '0'
300
341
  requirements: []
301
- rubyforge_project:
302
- rubygems_version: 2.5.1
303
- signing_key:
342
+ rubygems_version: 3.6.2
304
343
  specification_version: 4
305
344
  summary: Tries really, really hard not to push files to S3.
306
345
  test_files: