Files
picopackage/lib/picopackage/cache.rb
T
Dan MilneandClaude Fable 5 6dd57e84f1 Version 0.3.0: resolver/merge/cache refactor, signing design notes
- Split the monolith into package/provider/fetch/merge/cache modules with
  the Resolver deciding install/adopt/update/merge/conflict outcomes
- Three-way merges via git merge-file/diff3 against a content-addressed
  merge-base cache; conflicts go to a .picopackage-merge sibling
- Tests for package, provider, resolver, merge, and cache
- notes.md: update UX and signing design — diff-by-default, SSH signature
  identity pinning (TOFU), key changes as a hard stop, exit-code contract
- Add CLAUDE.md; remove the pre-refactor exe/pppkg monolith and scratch files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EiyJC73Mz8xZyCTvCEY8qn
2026-08-13 21:18:19 +10:00

61 lines
1.8 KiB
Ruby

# frozen_string_literal: true
require "fileutils"
module Picopackage
# Content-addressed store of upstream payloads, kept so that updates have a
# merge base to work from.
#
# A three-way merge needs the common ancestor: the payload exactly as upstream
# last sent it. Metadata already records that payload's checksum in
# `base_checksum`, so the checksum doubles as the cache key and a package
# carries the pointer to its own ancestor.
class Cache
def self.default_root
ENV["PICOPACKAGE_CACHE"] ||
File.join(ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache"), "picopackage")
end
attr_reader :root
def initialize(root: self.class.default_root)
@root = root
end
# Returns the checksum the payload was stored under, for recording as a
# `base_checksum`.
def store(payload)
Payload.checksum(payload).tap do |checksum|
path = path_for(checksum)
next if File.exist?(path)
FileUtils.mkdir_p(File.dirname(path))
# Written via a sibling tempfile and renamed so a concurrent reader
# never sees a half-written ancestor.
tmp = "#{path}.#{Process.pid}.tmp"
File.write(tmp, payload)
File.rename(tmp, path)
end
end
def fetch(checksum)
path = path_for(checksum)
File.read(path) if path && File.exist?(path)
end
def include?(checksum) = !fetch(checksum).nil?
private
# "sha256:abcdef…" => <root>/sha256/ab/cdef…
def path_for(checksum)
return nil if checksum.nil? || checksum.empty?
algorithm, digest = checksum.split(":", 2)
return nil if digest.nil? || digest.length < 3 || !digest.match?(/\A[a-f0-9]+\z/i)
File.join(root, algorithm, digest[0, 2], digest[2..])
end
end
end