From ce11ecb1860a778f03e7b347470ce54127b3f50b Mon Sep 17 00:00:00 2001 From: Tom Proctor Date: Wed, 26 Nov 2025 09:01:22 +0000 Subject: [PATCH] WIP Change-Id: Iecd5b2efb16f682dbb0865298fd6a2d78a63baf1 Signed-off-by: Tom Proctor --- cmd/cigocacher/cigocacher.go | 5 +- cmd/cigocacher/disk.go | 184 +++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 cmd/cigocacher/disk.go diff --git a/cmd/cigocacher/cigocacher.go b/cmd/cigocacher/cigocacher.go index b38df4c2b..ffe964b52 100644 --- a/cmd/cigocacher/cigocacher.go +++ b/cmd/cigocacher/cigocacher.go @@ -29,7 +29,6 @@ import ( "time" "github.com/bradfitz/go-tool-cache/cacheproc" - "github.com/bradfitz/go-tool-cache/cachers" ) func main() { @@ -66,7 +65,7 @@ func main() { } c := &cigocacher{ - disk: &cachers.DiskCache{Dir: d}, + disk: &DiskCache{Dir: d}, verbose: *verbose, } if *cigocachedURL != "" { @@ -115,7 +114,7 @@ func httpClient() *http.Client { } type cigocacher struct { - disk *cachers.DiskCache + disk *DiskCache gocached *gocachedClient verbose bool diff --git a/cmd/cigocacher/disk.go b/cmd/cigocacher/disk.go new file mode 100644 index 000000000..68235af6a --- /dev/null +++ b/cmd/cigocacher/disk.go @@ -0,0 +1,184 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package main + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + "path/filepath" + "runtime" + "time" +) + +// indexEntry is the metadata that DiskCache stores on disk for an ActionID. +type indexEntry struct { + Version int `json:"v"` + OutputID string `json:"o"` + Size int64 `json:"n"` + TimeNanos int64 `json:"t"` +} + +type DiskCache struct { + Dir string + Verbose bool + Logf func(format string, args ...any) // optional alt logger +} + +func (dc *DiskCache) logf(format string, args ...any) { + if dc.Logf != nil { + dc.Logf(format, args...) + } else if dc.Verbose { + log.Printf(format, args...) + } +} + +func (dc *DiskCache) Get(ctx context.Context, actionID string) (outputID, diskPath string, err error) { + if !validHex(actionID) { + return "", "", fmt.Errorf("actionID must be valid hex strings") + } + + actionFile := dc.ActionFilename(actionID) + ij, err := os.ReadFile(actionFile) + if err != nil { + if os.IsNotExist(err) { + err = nil + if dc.Verbose { + dc.logf("disk miss: %v", actionID) + } + } + return "", "", err + } + var ie indexEntry + if err := json.Unmarshal(ij, &ie); err != nil { + dc.logf("Warning: JSON error for action %q: %v", actionID, err) + return "", "", nil + } + if !validHex(ie.OutputID) { + // Protect against malicious non-hex OutputID on disk + return "", "", nil + } + return ie.OutputID, dc.OutputFilename(ie.OutputID), nil +} + +func (dc *DiskCache) OutputFilename(outputID string) string { + if !validHex(outputID) { + return "" + } + return filepath.Join(dc.Dir, outputID[:2], fmt.Sprintf("o-%s", outputID)) +} + +func (dc *DiskCache) ActionFilename(actionID string) string { + if !validHex(actionID) { + return "" + } + return filepath.Join(dc.Dir, actionID[:2], fmt.Sprintf("a-%s", actionID)) +} + +func validHex(x string) bool { + if len(x) < 4 || len(x) > 100 { + return false + } + for _, b := range x { + if b >= '0' && b <= '9' || b >= 'a' && b <= 'f' { + continue + } + return false + } + return true +} + +func (dc *DiskCache) Put(ctx context.Context, actionID, outputID string, size int64, body io.Reader) (diskPath string, _ error) { + if len(actionID) < 4 || len(outputID) < 4 { + return "", fmt.Errorf("actionID and outputID must be at least 4 characters long") + } + if !validHex(actionID) { + log.Printf("diskcache: got invalid actionID %q", actionID) + return "", errors.New("actionID must be hex") + } + if !validHex(outputID) { + log.Printf("diskcache: got invalid outputID %q", outputID) + return "", errors.New("outputID must be hex") + } + + actionFile := dc.ActionFilename(actionID) + outputFile := dc.OutputFilename(outputID) + actionDir := filepath.Dir(actionFile) + outputDir := filepath.Dir(outputFile) + + if err := os.MkdirAll(actionDir, 0755); err != nil { + return "", fmt.Errorf("failed to create action directory: %w", err) + } + if err := os.MkdirAll(outputDir, 0755); err != nil { + return "", fmt.Errorf("failed to create output directory: %w", err) + } + + // Special case empty files; they're both common and easier to do race-free. + if size == 0 { + zf, err := os.OpenFile(outputFile, os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + return "", err + } + zf.Close() + } else { + wrote, err := writeAtomic(outputFile, body) + if err != nil { + return "", err + } + if wrote != size { + return "", fmt.Errorf("wrote %d bytes, expected %d", wrote, size) + } + } + + ij, err := json.Marshal(indexEntry{ + Version: 1, + OutputID: outputID, + Size: size, + TimeNanos: time.Now().UnixNano(), + }) + if err != nil { + return "", err + } + if _, err := writeAtomic(actionFile, bytes.NewReader(ij)); err != nil { + return "", err + } + return outputFile, nil +} + +func writeAtomic(dest string, r io.Reader) (int64, error) { + tf, err := os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".*") + if err != nil { + return 0, err + } + size, err := io.Copy(tf, r) + if err != nil { + tf.Close() + os.Remove(tf.Name()) + return 0, err + } + if err := tf.Close(); err != nil { + os.Remove(tf.Name()) + return 0, err + } + if err := os.Rename(tf.Name(), dest); err != nil { + os.Remove(tf.Name()) + if runtime.GOOS == "windows" { + if st, statErr := os.Stat(dest); statErr == nil && st.Size() == size { + return size, nil + } else { + log.Printf("DEBUG: %v", statErr) + if st != nil { + log.Printf("DEBUG: size=%d, wanted %d", st.Size(), size) + } + } + } + return 0, err + } + return size, nil +}