Class: Middleman::S3Sync::Resource

Inherits:
Object
  • Object
show all
Includes:
Status
Defined in:
lib/middleman/s3_sync/resource.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Status

#say_status

Constructor Details

#initialize(resource, partial_s3_resource, path: nil) ⇒ Resource

Returns a new instance of Resource.



9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/middleman/s3_sync/resource.rb', line 9

def initialize(resource, partial_s3_resource, path: nil)
  @resource = resource
  @path = if path
            path.sub(/^\//, '')
          elsif resource
            resource.destination_path.sub(/^\//, '')
          elsif partial_s3_resource&.key
            partial_s3_resource.key.sub(/^\//, '')
          else
            ''
          end
  @partial_s3_resource = partial_s3_resource
end

Instance Attribute Details

#content_typeObject

Returns the value of attribute content_type.



4
5
6
# File 'lib/middleman/s3_sync/resource.rb', line 4

def content_type
  @content_type
end

#full_s3_resourceObject

S3 resource as returned by a HEAD request



28
29
30
# File 'lib/middleman/s3_sync/resource.rb', line 28

def full_s3_resource
  @full_s3_resource
end

#gzippedObject

Returns the value of attribute gzipped.



4
5
6
# File 'lib/middleman/s3_sync/resource.rb', line 4

def gzipped
  @gzipped
end

#options=(value) ⇒ Object

Sets the attribute options

Parameters:

  • value

    the value to set the attribute options to.



4
5
6
# File 'lib/middleman/s3_sync/resource.rb', line 4

def options=(value)
  @options = value
end

#partial_s3_resourceObject

Returns the value of attribute partial_s3_resource.



4
5
6
# File 'lib/middleman/s3_sync/resource.rb', line 4

def partial_s3_resource
  @partial_s3_resource
end

#pathObject

Returns the value of attribute path.



4
5
6
# File 'lib/middleman/s3_sync/resource.rb', line 4

def path
  @path
end

#resourceObject

Returns the value of attribute resource.



4
5
6
# File 'lib/middleman/s3_sync/resource.rb', line 4

def resource
  @resource
end

Instance Method Details

#alternate_encoding?Boolean

Returns:

  • (Boolean)


163
164
165
# File 'lib/middleman/s3_sync/resource.rb', line 163

def alternate_encoding?
  status == :alternate_encoding
end

#caching_policyObject



349
350
351
# File 'lib/middleman/s3_sync/resource.rb', line 349

def caching_policy
  @caching_policy ||= Middleman::S3Sync.caching_policy_for(content_type)
end

#caching_policy_match?Boolean

Returns:

  • (Boolean)


353
354
355
356
357
358
359
# File 'lib/middleman/s3_sync/resource.rb', line 353

def caching_policy_match?
  if caching_policy && full_s3_resource && full_s3_resource.respond_to?(:cache_control)
    caching_policy.cache_control == full_s3_resource.cache_control
  else
    true
  end
end

#compute_md5s_single_readObject

Compute both MD5s from a single file read when they’re the same file



315
316
317
318
319
320
321
322
# File 'lib/middleman/s3_sync/resource.rb', line 315

def compute_md5s_single_read
  return if @md5s_computed
  content = File.read(local_path)
  md5 = Digest::MD5.hexdigest(content)
  @local_object_md5 = md5
  @local_content_md5 = md5
  @md5s_computed = true
end

#create!Object



112
113
114
115
116
117
# File 'lib/middleman/s3_sync/resource.rb', line 112

def create!
  say_status "#{ANSI.green{"Creating"}} #{remote_path}#{ gzipped ? ANSI.white {' (gzipped)'} : ''}"
  unless options.dry_run
    upload!
  end
end

#destroy!Object



107
108
109
110
# File 'lib/middleman/s3_sync/resource.rb', line 107

def destroy!
  say_status "#{ANSI.red{"Deleting"}} #{remote_path}"
  bucket.object(remote_path.sub(/^\//, '')).delete unless options.dry_run
end

#detect_content_type_from_extensionObject



341
342
343
344
345
346
347
# File 'lib/middleman/s3_sync/resource.rb', line 341

def detect_content_type_from_extension
  return nil unless defined?(MIME::Types)
  extension = File.extname(original_path).delete_prefix('.')
  return nil if extension.empty?
  types = MIME::Types.type_for(extension)
  types.first&.content_type
end

#directory?Boolean

Returns:

  • (Boolean)


266
267
268
# File 'lib/middleman/s3_sync/resource.rb', line 266

def directory?
  File.directory?(local_path)
end

#encoding_match?Boolean

Returns:

  • (Boolean)


278
279
280
# File 'lib/middleman/s3_sync/resource.rb', line 278

def encoding_match?
  (options.prefer_gzip && gzipped && full_s3_resource.content_encoding == 'gzip') || (!options.prefer_gzip && !gzipped && !full_s3_resource.content_encoding )
end

#identical?Boolean

Returns:

  • (Boolean)


167
168
169
# File 'lib/middleman/s3_sync/resource.rb', line 167

def identical?
  status == :identical
end

#ignore!Object



144
145
146
147
148
149
150
151
152
153
# File 'lib/middleman/s3_sync/resource.rb', line 144

def ignore!
  if options.verbose
    reason = if redirect?
               :redirect
             elsif directory?
               :directory
             end
    say_status "#{ANSI.yellow{"Ignoring"}} #{remote_path} #{ reason ? ANSI.white {"(#{reason})" } : "" }"
  end
end

#local?Boolean

Returns:

  • (Boolean)


228
229
230
231
# File 'lib/middleman/s3_sync/resource.rb', line 228

def local?
  # For orphan files (scan_build_dir), resource is nil but file exists
  File.exist?(local_path)
end

#local_contentObject



179
180
181
182
183
184
185
# File 'lib/middleman/s3_sync/resource.rb', line 179

def local_content
  if block_given?
    File.open(local_path) { |f| yield f.read }
  else
    File.read(local_path)
  end
end

#local_content_md5Object



300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/middleman/s3_sync/resource.rb', line 300

def local_content_md5
  @local_content_md5 ||= begin
    # When not gzipped, compute both MD5s in single read to avoid redundant I/O
    if !gzipped && local_path == original_path
      compute_md5s_single_read
      @local_content_md5
    elsif File.exist?(original_path)
      Digest::MD5.hexdigest(File.read(original_path))
    else
      nil
    end
  end
end

#local_object_md5Object



288
289
290
291
292
293
294
295
296
297
298
# File 'lib/middleman/s3_sync/resource.rb', line 288

def local_object_md5
  @local_object_md5 ||= begin
    # When not gzipped, compute both MD5s in single read to avoid redundant I/O
    if !gzipped && local_path == original_path
      compute_md5s_single_read
      @local_object_md5
    else
      Digest::MD5.hexdigest(File.read(local_path))
    end
  end
end

#local_pathObject



98
99
100
101
102
103
104
105
# File 'lib/middleman/s3_sync/resource.rb', line 98

def local_path
  local_path = build_dir + '/' + path.gsub(/^#{options.prefix}/, '')
  if options.prefer_gzip && File.exist?(local_path + ".gz")
    @gzipped = true
    local_path += ".gz"
  end
  local_path
end

#metadata_match?Boolean

Returns:

  • (Boolean)


242
243
244
# File 'lib/middleman/s3_sync/resource.rb', line 242

def 
  redirect_match? && caching_policy_match?
end

#normalize_path(prefix, path) ⇒ Object



50
51
52
53
54
55
# File 'lib/middleman/s3_sync/resource.rb', line 50

def normalize_path(prefix, path)
  # Remove any trailing slash from prefix and leading slash from path
  prefix = prefix.chomp('/')
  path = path.sub(/^\//, '')
  "#{prefix}/#{path}"
end

#original_pathObject



324
325
326
# File 'lib/middleman/s3_sync/resource.rb', line 324

def original_path
  gzipped ? local_path.gsub(/\.gz$/, '') : local_path
end

#redirect?Boolean

Returns:

  • (Boolean)


237
238
239
240
# File 'lib/middleman/s3_sync/resource.rb', line 237

def redirect?
  !!(resource && resource.respond_to?(:redirect?) && resource.redirect?) || 
    !!(full_s3_resource && full_s3_resource.respond_to?(:website_redirect_location) && full_s3_resource.website_redirect_location)
end

#redirect_match?Boolean

Returns:

  • (Boolean)


246
247
248
249
250
251
252
# File 'lib/middleman/s3_sync/resource.rb', line 246

def redirect_match?
  if redirect?
    redirect_url == remote_redirect_url
  else
    true
  end
end

#redirect_urlObject



262
263
264
# File 'lib/middleman/s3_sync/resource.rb', line 262

def redirect_url
  resource.respond_to?(:target_url) ? resource.target_url : nil
end

#relative_pathObject



270
271
272
# File 'lib/middleman/s3_sync/resource.rb', line 270

def relative_path
  @relative_path ||= local_path.gsub(/#{build_dir}/, '')
end

#remote?Boolean

Returns:

  • (Boolean)


233
234
235
# File 'lib/middleman/s3_sync/resource.rb', line 233

def remote?
  !full_s3_resource.nil?
end

#remote_content_md5Object



282
283
284
285
286
# File 'lib/middleman/s3_sync/resource.rb', line 282

def remote_content_md5
  if full_s3_resource && full_s3_resource.
    full_s3_resource.['content-md5']
  end
end

#remote_object_md5Object



274
275
276
# File 'lib/middleman/s3_sync/resource.rb', line 274

def remote_object_md5
  s3_resource.etag.gsub(/"/, '') if s3_resource.etag
end

#remote_pathObject Also known as: key



36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/middleman/s3_sync/resource.rb', line 36

def remote_path
  if s3_resource
    if s3_resource.respond_to?(:key)
      s3_resource.key.sub(/^\//, '')
    else
      # For HeadObjectOutput objects which don't have key method
      options.prefix ? normalize_path(options.prefix, path) : path.sub(/^\//, '')
    end
  else
    options.prefix ? normalize_path(options.prefix, path) : path.sub(/^\//, '')
  end.sub(/^\//, '')  # Ensure no leading slash
end

#remote_redirect_urlObject



258
259
260
# File 'lib/middleman/s3_sync/resource.rb', line 258

def remote_redirect_url
  full_s3_resource&.website_redirect_location
end

#s3_resourceObject



23
24
25
# File 'lib/middleman/s3_sync/resource.rb', line 23

def s3_resource
  @full_s3_resource || @partial_s3_resource
end

#shunned?Boolean

Returns:

  • (Boolean)


254
255
256
# File 'lib/middleman/s3_sync/resource.rb', line 254

def shunned?
  !!path[Regexp.union(options.ignore_paths)]
end

#statusObject



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/middleman/s3_sync/resource.rb', line 187

def status
  @status ||= if shunned?
                :ignored
              elsif directory?
                if remote?
                  :deleted
                else
                  :ignored
                end
              elsif local? && remote?
                if options.force
                  :updated
                elsif not 
                  :updated
                elsif local_object_md5 == remote_object_md5
                  :identical
                else
                  if !gzipped
                    # we're not gzipped, object hashes being different indicates updated content
                    :updated
                  elsif !encoding_match? || local_content_md5 != remote_content_md5
                    # we're gzipped, so we checked the content MD5, and it also changed
                    :updated
                  else
                    # we're gzipped, the object hashes differ, but the content hashes are equal
                    # this means the gzipped bits changed while the original bits did not
                    # what's more, we spent a HEAD request to find out
                    :alternate_encoding
                  end
                end
              elsif local?
                :new
              elsif remote? && redirect?
                :ignored
              elsif remote?
                :deleted
              else
                :ignored
              end
end

#to_create?Boolean

Returns:

  • (Boolean)


159
160
161
# File 'lib/middleman/s3_sync/resource.rb', line 159

def to_create?
  status == :new
end

#to_delete?Boolean

Returns:

  • (Boolean)


155
156
157
# File 'lib/middleman/s3_sync/resource.rb', line 155

def to_delete?
  status == :deleted
end

#to_hObject Also known as: attributes



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/middleman/s3_sync/resource.rb', line 57

def to_h
  attributes = {
    :key => key,
    :content_type => content_type,
    'content-md5' => local_content_md5
  }
  # Only add ACL if enabled (not for buckets with ACLs disabled)
  attributes[:acl] = options.acl if options.acl_enabled?

  if caching_policy
    attributes[:cache_control] = caching_policy.cache_control if caching_policy.cache_control
    attributes[:expires] = caching_policy.expires if caching_policy.expires
  end

  if options.prefer_gzip && gzipped
    attributes[:content_encoding] = "gzip"
  end

  if options.reduced_redundancy_storage
    attributes[:storage_class] = 'REDUCED_REDUNDANCY'
  end

  if options.encryption
    attributes[:encryption] = 'AES256'
  end

  if redirect?
    attributes['website-redirect-location'] = redirect_url
  end

  attributes
end

#to_ignore?Boolean

Returns:

  • (Boolean)


175
176
177
# File 'lib/middleman/s3_sync/resource.rb', line 175

def to_ignore?
  status == :ignored || status == :alternate_encoding
end

#to_update?Boolean

Returns:

  • (Boolean)


171
172
173
# File 'lib/middleman/s3_sync/resource.rb', line 171

def to_update?
  status == :updated
end

#update!Object



91
92
93
94
95
96
# File 'lib/middleman/s3_sync/resource.rb', line 91

def update!
  say_status "#{ANSI.blue{"Updating"}} #{remote_path}#{ gzipped ? ANSI.white {' (gzipped)'} : ''}"
  unless options.dry_run
    upload!
  end
end

#upload!Object



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/middleman/s3_sync/resource.rb', line 119

def upload!
  object = bucket.object(remote_path.sub(/^\//, ''))
  
  # Use streaming upload for memory efficiency with large files
  File.open(local_path, 'rb') do |file|
    upload_options = build_upload_options_for_stream(file)
    
    begin
      object.put(upload_options)
    rescue Aws::S3::Errors::AccessControlListNotSupported => e
      # Bucket has ACLs disabled - retry without ACL
      if upload_options.key?(:acl)
        say_status "#{ANSI.yellow{"Note"}} Bucket does not support ACLs, retrying without ACL parameter"
        # Automatically disable ACLs for this bucket going forward
        options.acl = ''
        upload_options.delete(:acl)
        file.rewind  # Reset file position for retry
        retry
      else
        raise e
      end
    end
  end
end