Module: DocOpsLab::Dev::Library::Cache

Defined in:
lib/docopslab/dev/library/cache.rb

Overview

Manages the host-wide library cache at ~/.cache/docopslab/dev/library/.

Cache layout:

current/    Active library used for sync and resolve operations.
previous/   Previous snapshot retained for fast rollback.

The XDG_CACHE_HOME env variable is respected; defaults to ~/.cache.

Constant Summary collapse

CATALOG_JSON_KEYS =
%w[library_version library_ref generated_at files].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.root_override=(value) ⇒ Object (writeonly)

Optional path override; set by Library.sync!/fetch! when manifest specifies ‘library.sync.cache_root`. Cleared after the operation.



22
23
24
# File 'lib/docopslab/dev/library/cache.rb', line 22

def root_override=(value)
  @root_override = value
end

Class Method Details

.available?Boolean

True if a current snapshot with a readable catalog is present.

Returns:

  • (Boolean)


67
68
69
# File 'lib/docopslab/dev/library/cache.rb', line 67

def available?
  File.exist?(catalog_path)
end

.catalogObject

Load and return the parsed catalog from the current snapshot. Returns nil if no cache is present or the catalog is unreadable.



60
61
62
63
64
# File 'lib/docopslab/dev/library/cache.rb', line 60

def catalog
  return nil unless File.exist?(catalog_path)

  load_catalog_json(catalog_path)
end

.catalog_pathObject

Absolute path to the catalog inside the current snapshot.



54
55
56
# File 'lib/docopslab/dev/library/cache.rb', line 54

def catalog_path
  File.join(current_path, 'catalog.json')
end

.current_pathObject

Absolute path to the current library snapshot.



44
45
46
# File 'lib/docopslab/dev/library/cache.rb', line 44

def current_path
  File.join(root, 'current')
end

.fresh?(max_age_hours = 24) ⇒ Boolean

True if the cache exists and was generated within max_age_hours.

Returns:

  • (Boolean)


88
89
90
91
92
93
94
95
96
97
# File 'lib/docopslab/dev/library/cache.rb', line 88

def fresh? max_age_hours=24
  return false unless available?

  ts = catalog&.dig('generated_at')
  return false unless ts

  (Time.now.utc - Time.parse(ts)) < max_age_hours * 3600
rescue ArgumentError
  false
end

.previous_pathObject

Absolute path to the previous library snapshot (rollback target).



49
50
51
# File 'lib/docopslab/dev/library/cache.rb', line 49

def previous_path
  File.join(root, 'previous')
end

.rollback!Object

Swap previous/ back to current/. Returns false if no previous/ snapshot exists.



123
124
125
126
127
128
129
# File 'lib/docopslab/dev/library/cache.rb', line 123

def rollback!
  return false unless Dir.exist?(previous_path)

  FileUtils.rm_rf(current_path)
  FileUtils.mv(previous_path, current_path)
  true
end

.rootObject

Absolute path to the cache root (~/.cache/docopslab/dev/library/). Respects root_override if set, then $XDG_CACHE_HOME, then ~/.cache.



26
27
28
29
30
31
# File 'lib/docopslab/dev/library/cache.rb', line 26

def root
  return File.expand_path(@root_override) if @root_override

  base = ENV.fetch('XDG_CACHE_HOME', File.join(Dir.home, '.cache'))
  File.join(base, Dev.xdg_cache_subpath)
end

.rotate!Object

Rotate current/ to previous/, removing any prior previous/ snapshot. Returns true if a rotation was performed, false if current/ was absent.



101
102
103
104
105
106
107
# File 'lib/docopslab/dev/library/cache.rb', line 101

def rotate!
  return false unless Dir.exist?(current_path)

  FileUtils.rm_rf(previous_path)
  FileUtils.mv(current_path, previous_path)
  true
end

.statusObject

Return a status hash describing the current snapshot.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/docopslab/dev/library/cache.rb', line 132

def status
  if available?
    meta = catalog
    {
      available: true,
      version: meta&.dig('library_version'),
      ref: meta&.dig('library_ref'),
      generated_at: meta&.dig('generated_at'),
      cache_path: current_path,
      has_previous: Dir.exist?(previous_path)
    }
  else
    { available: false, cache_path: current_path }
  end
end

.stored_headObject

The remote HEAD SHA stored from the last successful fetch, or nil.



72
73
74
75
76
77
78
79
# File 'lib/docopslab/dev/library/cache.rb', line 72

def stored_head
  return nil unless File.exist?(head_path)

  s = File.read(head_path).strip
  s.empty? ? nil : s
rescue StandardError
  nil
end

.with_root_override(path) ⇒ Object

Temporarily override the cache root for the duration of a block. Restores the previous value even if the block raises.



35
36
37
38
39
40
41
# File 'lib/docopslab/dev/library/cache.rb', line 35

def with_root_override path
  previous = @root_override
  @root_override = path
  yield
ensure
  @root_override = previous
end

.write!(source_dir) ⇒ Object

Install a directory as the new current/ snapshot. Rotates any existing current/ to previous/ first. source_dir must be an existing directory.

Raises:

  • (ArgumentError)


112
113
114
115
116
117
118
119
# File 'lib/docopslab/dev/library/cache.rb', line 112

def write! source_dir
  raise ArgumentError, "Source directory not found: #{source_dir}" unless Dir.exist?(source_dir)

  rotate! if Dir.exist?(current_path)
  FileUtils.mkdir_p(File.dirname(current_path))
  FileUtils.cp_r(source_dir, current_path)
  true
end

.write_head!(sha) ⇒ Object

Persist the remote HEAD SHA alongside the cache.



82
83
84
85
# File 'lib/docopslab/dev/library/cache.rb', line 82

def write_head! sha
  FileUtils.mkdir_p(root)
  File.write(head_path, sha.to_s.strip)
end