Files
upstream-mirror 0f4df23290
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

139 lines
4.3 KiB
Go

package cache
import (
"errors"
"fmt"
"io/fs"
"math/rand/v2"
"os"
"path/filepath"
"strconv"
)
// ErrNotFound is returned when a blob or action result is not in the cache.
var ErrNotFound = errors.New("cache: not found")
// CAS is an on-disk content-addressable store keyed by SHA-256, modeled on
// Bazel's disk cache. Blobs live at cas/<xx>/<hash> under the cache root,
// where <xx> is the first two hex characters of the hash. Because a blob's
// name is derived from its contents, entries never change once written:
// writers race benignly and readers can detect corruption by re-hashing.
//
// Content with an externally declared checksum (remotely fetched plugins)
// needs no action cache entry: the declared sha256 is the address, so it is
// stored and loaded directly — see SHA256Digest.
//
// All I/O goes through an os.Root, so no entry name — hash-derived or read
// from an action cache entry — can escape the cache directory.
type CAS struct {
root *os.Root
}
func newCAS(root *os.Root) (*CAS, error) {
if err := root.MkdirAll("tmp", 0755); err != nil {
return nil, fmt.Errorf("cache: create tmp: %w", err)
}
return &CAS{root: root}, nil
}
// path returns a blob's path relative to the cache root.
func (c *CAS) path(d Digest) string {
return filepath.Join("cas", d.Hash[:2], d.Hash)
}
// Filename returns the path of a stored blob, for a consumer that needs the
// file rather than its bytes — SQLite, for one, opens a database by name. A
// blob is named after the hash of its contents, so the file at this path never
// changes and any number of processes may read it at once.
//
// It reports false when the blob is not stored.
func (c *CAS) Filename(d Digest) (string, bool) {
if !c.Contains(d) {
return "", false
}
return filepath.Join(c.root.Name(), c.path(d)), true
}
// createTemp creates a staging file under tmp/ in the cache root, returning
// the open file and its root-relative name.
func (c *CAS) createTemp(prefix string) (*os.File, string, error) {
for range 10000 {
name := filepath.Join("tmp", prefix+strconv.FormatUint(rand.Uint64(), 36))
f, err := c.root.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
if errors.Is(err, fs.ErrExist) {
continue
}
return f, name, err
}
return nil, "", errors.New("cache: could not create temp file")
}
// Put stores a blob and returns its digest. Writing is atomic: the blob is
// staged in a temp file and renamed into place, so concurrent sqlc processes
// never observe partial entries.
func (c *CAS) Put(data []byte) (Digest, error) {
d := DigestOf(data)
path := c.path(d)
// Skip the write only when an entry of the right size already exists; a
// wrong-sized entry is corrupt and is atomically replaced by the rename
// below.
if fi, err := c.root.Stat(path); err == nil && fi.Size() == d.SizeBytes {
return d, nil
}
if err := c.root.MkdirAll(filepath.Dir(path), 0755); err != nil {
return Digest{}, fmt.Errorf("cache: %w", err)
}
f, name, err := c.createTemp(d.Hash[:8] + "-")
if err != nil {
return Digest{}, fmt.Errorf("cache: %w", err)
}
defer c.root.Remove(name)
if _, err := f.Write(data); err != nil {
f.Close()
return Digest{}, fmt.Errorf("cache: %w", err)
}
if err := f.Close(); err != nil {
return Digest{}, fmt.Errorf("cache: %w", err)
}
if err := c.root.Rename(name, path); err != nil {
return Digest{}, fmt.Errorf("cache: %w", err)
}
return d, nil
}
// Get returns the blob for a digest. Contents are re-hashed before being
// returned; a corrupt entry is evicted and reported as ErrNotFound so
// callers simply redo the work that produced it.
func (c *CAS) Get(d Digest) ([]byte, error) {
if !d.valid() {
return nil, ErrNotFound
}
path := c.path(d)
data, err := c.root.ReadFile(path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("cache: %w", err)
}
if DigestOf(data).Hash != d.Hash {
c.root.Remove(path)
return nil, ErrNotFound
}
return data, nil
}
// Contains reports whether a blob with the given digest is present, checking
// size when the digest carries one. It does not verify contents; Get performs
// full verification.
func (c *CAS) Contains(d Digest) bool {
if !d.valid() {
return false
}
fi, err := c.root.Stat(c.path(d))
if err != nil {
return false
}
return d.SizeBytes < 0 || fi.Size() == d.SizeBytes
}