Class: JekyllImgFlow::ManifestManager

Inherits:
Object
  • Object
show all
Defined in:
lib/jekyll-imgflow/manifest_manager.rb

Overview

Manages the manifest of generated images Tracks default vs specialized versions and page usage for cleanup

Constant Summary collapse

VERSION_TYPES =
%w[default specialized].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(site) ⇒ ManifestManager

Returns a new instance of ManifestManager.



13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 13

def initialize(site)
  @site = site
  @config = JekyllImgFlow::Config.new(site)
  @file_cleaner = GeneratedFileCleaner.new(site, @config)
  @manifest_path = File.join(@site.source, @config.cache_dir, "imgflow-manifest.json")
  @legacy_manifest_paths = legacy_manifest_paths
  @manifest = load_manifest(manifest_load_path)
  @current_provider = current_provider

  # Check if provider changed and invalidate cache if needed
  handle_provider_change if provider_changed?
end

Instance Attribute Details

#manifest_pathObject (readonly)

Returns the value of attribute manifest_path.



11
12
13
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 11

def manifest_path
  @manifest_path
end

Instance Method Details

#cleanup_deleted_originals(current_originals) ⇒ Object

Clean up manifest entries for deleted original images

Parameters:

  • current_originals (Array<String>)

    Paths relative to the originals directory



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 106

def cleanup_deleted_originals(current_originals)
  current_set = current_originals.to_set

  @manifest.each_key do |original_name|
    next if current_set.include?(original_name)

    data = @manifest.delete(original_name)
    VERSION_TYPES.each do |type|
      (data.dig("versions", type) || []).each do |version|
        @file_cleaner.delete(version["output"])
      end
    end
    Jekyll.logger.debug "🗑️  Removed manifest entry for deleted original: #{original_name}"
  end
end

#cleanup_obsolete_defaults(expected_operations) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 122

def cleanup_obsolete_defaults(expected_operations)
  @manifest.each_value do |data|
    versions = data.dig("versions", "default") || []
    versions.reject! do |version|
      obsolete = expected_operations.none? do |operations|
        same_operations?(version["operations"], operations)
      end
      @file_cleaner.delete(version["output"]) if obsolete
      obsolete
    end
  end
end

#cleanup_orphansObject

Cleanup orphaned specialized images



328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 328

def cleanup_orphans
  orphans = find_orphans
  cleaned = []

  orphans.each do |orphan|
    if @file_cleaner.delete(orphan["output"])
      cleaned << orphan["output"]
      Jekyll.logger.info "🗑️  Cleaned orphaned image: #{orphan['output']}"
    end

    # Remove from manifest
    versions = @manifest.dig(orphan["original"], "versions", "specialized") || []
    versions.reject! { |v| same_operations?(v["operations"], orphan["operations"]) }
  end

  cleaned
end

#current_providerObject

Get current provider from site config



102
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 102

def current_provider = @config.backend_priority&.first || "unknown"

#default_version?(original_name, operations) ⇒ Boolean

Check if image version is default type

Returns:

  • (Boolean)


288
289
290
291
292
293
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 288

def default_version?(original_name, operations)
  return false unless @manifest[original_name]

  versions = @manifest.dig(original_name, "versions", "default") || []
  versions.any? { |v| same_operations?(v["operations"], operations) }
end

#empty_manifestObject



52
53
54
55
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 52

def empty_manifest
  @cached_provider = nil
  {}
end

#find_orphansObject

Find orphaned specialized images (not used on any page)



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 296

def find_orphans
  orphans = []

  @manifest.each do |original_name, data|
    specialized = data.dig("versions", "specialized") || []
    specialized.each do |version|
      next unless version["used_on"].nil? || version["used_on"].empty?

      orphans << {
        "original" => original_name,
        "output" => version["output"],
        "operations" => version["operations"]
      }
    end
  end

  orphans
end

#get_version_output(original_name, operations, type = :specialized) ⇒ Object

Get output path for a specific version



206
207
208
209
210
211
212
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 206

def get_version_output(original_name, operations, type = :specialized)
  return unless @manifest[original_name]

  versions = @manifest.dig(original_name, "versions", type.to_s) || []
  version = versions.find { |v| same_operations?(v["operations"], operations) }
  version&.dig("output")
end

#get_versions(original_name) ⇒ Object

Get all versions for an original image



347
348
349
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 347

def get_versions(original_name)
  @manifest.dig(original_name, "versions") || { "default" => [], "specialized" => [] }
end

#handle_provider_changeObject

Handle provider change by invalidating cache and cleaning optimized directory



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 187

def handle_provider_change
  Jekyll.logger.warn "ImgFlow:",
                     "Provider changed from '#{@cached_provider}' to '#{@current_provider}'"
  Jekyll.logger.warn "ImgFlow:", "Invalidating cache and cleaning optimized directory..."

  deleted_count = @file_cleaner.clear
  Jekyll.logger.warn "ImgFlow:",
                     "Deleted #{deleted_count} cached images from optimized directory"

  # Clear manifest cache
  @manifest.clear
  Jekyll.logger.warn "ImgFlow:",
                     "Manifest cache cleared - all images will be regenerated with '#{@current_provider}'"

  # Update cached provider to current
  @cached_provider = @current_provider
end

#load_manifest(path = @manifest_path) ⇒ Object

Load existing manifest or create new one



27
28
29
30
31
32
33
34
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 27

def load_manifest(path = @manifest_path)
  return empty_manifest unless path && File.exist?(path)

  load_manifest_file(path)
rescue JSON::ParserError => e
  Jekyll.logger.warn "ImgFlow: Corrupt manifest file, starting fresh: #{e.message}"
  empty_manifest
end

#load_manifest_file(path) ⇒ Object



36
37
38
39
40
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 36

def load_manifest_file(path)
  data = JSON.parse(File.read(path))
  images = manifest_images(data)
  migrate_legacy_manifest(images)
end

#manifest_image(original_name, file_digest) ⇒ Object



252
253
254
255
256
257
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 252

def manifest_image(original_name, file_digest)
  @manifest[original_name] ||= {
    "versions" => { "default" => [], "specialized" => [] },
    "file_digest" => file_digest
  }
end

#manifest_images(data) ⇒ Object



42
43
44
45
46
47
48
49
50
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 42

def manifest_images(data)
  if data.is_a?(Hash) && data.key?("images")
    @cached_provider = data["provider"]
    data["images"] || {}
  else
    @cached_provider = nil
    data
  end
end

#migrate_legacy_manifest(images) ⇒ Object

Detect and migrate legacy manifest entries (pre-0.1.11 format). Old entries have operations like 400 without format/quality and no file_digest. Clearing them lets the build re-register existing output files without reprocessing (output_up_to_date? check handles this).



61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 61

def migrate_legacy_manifest(images)
  legacy = images.any? do |_name, data|
    versions = data.dig("versions", "default") || []
    versions.any? { |v| v["file_digest"].nil? || !v["operations"].key?("format") }
  end
  if legacy
    Jekyll.logger.info "ImgFlow: Detected legacy manifest format, " \
                       "clearing entries for re-registration."
    images.clear
  end
  images
end

#new_version(output_path, operations, type, page_paths, file_digest, provider) ⇒ Object



275
276
277
278
279
280
281
282
283
284
285
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 275

def new_version(output_path, operations, type, page_paths, file_digest, provider)
  {
    "output" => output_path,
    "operations" => operations,
    "type" => type.to_s,
    "used_on" => page_paths,
    "created_at" => Time.now.to_i,
    "file_digest" => file_digest,
    "provider" => provider
  }
end

#normalize_operations(operations) ⇒ Object

Normalize operations for consistent comparison after JSON persistence.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 144

def normalize_operations(operations)
  case operations
  when Hash
    operations.to_h do |key, value|
      [key.to_s, normalize_operations(value)]
    end
  when Array
    operations.map { |value| normalize_operations(value) }
  when Symbol
    operations.to_s
  else
    operations
  end
end

#normalize_page_paths(page_path) ⇒ Object



259
260
261
262
263
264
265
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 259

def normalize_page_paths(page_path)
  if page_path.is_a?(Array)
    page_path
  else
    (page_path ? [page_path] : [])
  end
end

#provider_changed?Boolean

Check if the provider has changed since the manifest was created

Returns:

  • (Boolean)


178
179
180
181
182
183
184
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 178

def provider_changed?
  # If no cached provider, this is a new manifest
  return false if @cached_provider.nil?

  # Provider changed if current doesn't match cached
  @cached_provider != @current_provider
end

#register_version(original_name, output_path, operations, type, page_path, file_digest = nil, provider = nil) ⇒ Object

Register a new image version



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 236

def register_version(original_name, output_path, operations, type, page_path,
                     file_digest = nil, provider = nil)
  image = manifest_image(original_name, file_digest)
  image["file_digest"] = file_digest if file_digest
  versions = image["versions"][type.to_s] ||= []
  normalized = normalize_operations(operations)
  page_paths = normalize_page_paths(page_path)
  version = versions.find { |entry| same_operations?(entry["operations"], normalized) }

  if version
    update_version(version, output_path, page_paths, file_digest, provider)
  else
    versions << new_version(output_path, normalized, type, page_paths, file_digest, provider)
  end
end

#remove_page_usage(page_path) ⇒ Object

Remove page from all image version usage tracking



316
317
318
319
320
321
322
323
324
325
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 316

def remove_page_usage(page_path)
  @manifest.each_value do |data|
    VERSION_TYPES.each do |type|
      versions = data.dig("versions", type) || []
      versions.each do |version|
        version["used_on"]&.delete(page_path)
      end
    end
  end
end

#reset_page_usageObject



135
136
137
138
139
140
141
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 135

def reset_page_usage
  @manifest.each_value do |data|
    (data.dig("versions", "specialized") || []).each do |version|
      version["used_on"] = []
    end
  end
end

#same_operations?(left, right) ⇒ Boolean

Returns:

  • (Boolean)


159
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 159

def same_operations?(left, right) = normalize_operations(left) == normalize_operations(right)

#saveObject

Save manifest to disk



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 75

def save
  # Store current provider at top level of manifest
  manifest_data = {
    "provider" => @current_provider,
    "images" => @manifest
  }
  content = JSON.pretty_generate(manifest_data)
  if File.exist?(@manifest_path) && File.binread(@manifest_path) == content
    cleanup_legacy_manifests
    return
  end

  directory = File.dirname(@manifest_path)
  temporary_path = "#{@manifest_path}.tmp-#{Process.pid}-#{Thread.current.object_id}"
  FileUtils.mkdir_p(directory)
  File.open(temporary_path, "wb") do |file|
    file.write(content)
    file.flush
    file.fsync
  end
  File.rename(temporary_path, @manifest_path)
  cleanup_legacy_manifests
ensure
  FileUtils.rm_f(temporary_path) if temporary_path
end

#update_page_usage(original_name, operations, type, page_path) ⇒ Object

Update page usage for an existing version This is a convenience method that calls register_version



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 216

def update_page_usage(original_name, operations, type, page_path)
  Jekyll.logger.debug "🔍 update_page_usage: original_name=#{original_name}, operations=#{operations.inspect}, type=#{type}, page_path=#{page_path}"

  # Get the existing version's output path (already relative)
  output_path = get_version_output(original_name, operations, type)
  Jekyll.logger.debug "🔍 update_page_usage: output_path=#{output_path}"
  return unless output_path

  # Get the existing version to preserve provider
  versions = @manifest.dig(original_name, "versions", type.to_s) || []
  version = versions.find { |v| same_operations?(v["operations"], operations) }
  provider = version&.dig("provider")
  Jekyll.logger.debug "🔍 update_page_usage: provider=#{provider}, current used_on=#{version&.dig('used_on')&.inspect}"

  # Call register_version to update page usage (output_path is already relative)
  register_version(original_name, output_path, operations, type, page_path, nil, provider)
  Jekyll.logger.debug "🔍 update_page_usage: completed register_version call"
end

#update_version(version, output_path, page_paths, file_digest, provider) ⇒ Object



267
268
269
270
271
272
273
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 267

def update_version(version, output_path, page_paths, file_digest, provider)
  version["used_on"] ||= []
  page_paths.each { |path| version["used_on"] << path if path && !version["used_on"].include?(path) }
  version["output"] = output_path
  version["file_digest"] = file_digest if file_digest
  version["provider"] = provider if provider
end

#version_exists?(original_name, operations, type = :specialized, file_digest = nil) ⇒ Boolean

Check if a specific image version exists and is up-to-date

Returns:

  • (Boolean)


162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 162

def version_exists?(original_name, operations, type = :specialized, file_digest = nil)
  return false unless @manifest[original_name]

  # Provider change is already handled in initialize, so just check operations
  versions = @manifest.dig(original_name, "versions", type.to_s) || []
  Jekyll.logger.debug "🔍 ManifestManager: Looking for operations: #{operations.inspect} in #{versions.length} #{type} versions"

  result = versions.any? do |version|
    same_operations?(version["operations"], operations) &&
      (!file_digest || version["file_digest"] == file_digest)
  end
  Jekyll.logger.debug "🔍 ManifestManager: Final result: #{result}"
  result
end

#versions?(original_name) ⇒ Boolean

Check if original image has any versions

Returns:

  • (Boolean)


352
353
354
355
356
357
# File 'lib/jekyll-imgflow/manifest_manager.rb', line 352

def versions?(original_name)
  return false unless @manifest[original_name]

  versions = @manifest[original_name]["versions"]
  (versions["default"]&.any? || false) || (versions["specialized"]&.any? || false)
end