- 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
61 lines
1.8 KiB
Ruby
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
|