~james/core

core/spindle/models/cache.go -rw-r--r-- 3.7 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 14 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package models

import (
	"fmt"
	"regexp"
	"runtime"
	"strings"

	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/google/uuid"
	"gopkg.in/yaml.v3"
)

type CacheEntry struct {
	Key      string   `yaml:"key"`
	Hash     []string `yaml:"hash"`
	Paths    []string `yaml:"paths"`
	Fallback bool     `yaml:"fallback"`
	// 1 is fastest, 19 is smallest, 0 is the zstd default
	CompressionLevel int `yaml:"compression-level"`
	// on-success is the default, always also saves on failed and timed-out runs
	When string `yaml:"when"`
}

type CacheBinding struct {
	EntryIndex       int
	Paths            []string
	Key              string
	Hash             string
	Checksum         string
	SizeBytes        int64
	SaveID           string
	SaveKey          string
	RestoreID        string
	RestoreKey       string
	RestoreName      string
	CompressionLevel int
	When             string
}

func (b CacheBinding) SaveOn(failed bool) bool {
	return b.When == "always" || !failed
}

func CacheNamespace() string {
	return fmt.Sprintf("tar-zstd-v1/%s/%s", runtime.GOOS, runtime.GOARCH)
}

func CacheObjectKey(repoDid, id string) (string, error) {
	did, err := syntax.ParseDID(repoDid)
	if err != nil {
		return "", fmt.Errorf("cache: invalid repository did %q: %w", repoDid, err)
	}
	parsedID, err := uuid.Parse(id)
	if err != nil {
		return "", fmt.Errorf("cache: invalid object id %q: %w", id, err)
	}
	return fmt.Sprintf("objects/%s/%s", did, parsedID), nil
}

// keys become storage paths, so no slashes
var cacheKeyRe = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`)

func (c CacheEntry) Validate() error {
	if !cacheKeyRe.MatchString(c.Key) {
		return fmt.Errorf("cache: invalid key %q (allowed: letters, digits, '.', '_', '-')", c.Key)
	}
	for _, f := range c.Hash {
		// rev-parse needs plain repo-relative paths, not pathspecs
		if f == "" || strings.HasPrefix(f, "/") || strings.HasPrefix(f, "..") {
			return fmt.Errorf("cache %q: hash path %q is not repo-relative", c.Key, f)
		}
		if strings.ContainsAny(f, ": \t\n\"'`$\\*?[") || strings.Contains(f, "/../") || strings.HasSuffix(f, "/..") {
			return fmt.Errorf("cache %q: hash path %q contains unsupported characters", c.Key, f)
		}
	}
	if len(c.Paths) == 0 {
		return fmt.Errorf("cache %q: no paths", c.Key)
	}
	if c.CompressionLevel < 0 || c.CompressionLevel > 19 {
		return fmt.Errorf("cache %q: compression-level %d out of range (1-19)", c.Key, c.CompressionLevel)
	}
	switch c.When {
	case "", "on-success", "always":
	default:
		return fmt.Errorf("cache %q: when %q is not one of on-success, always", c.Key, c.When)
	}
	seen := make(map[string]bool, len(c.Paths))
	for _, p := range c.Paths {
		if p == "" {
			return fmt.Errorf("cache %q: empty path", c.Key)
		}
		if strings.ContainsAny(p, " \t\n\"'`$\\;&|<>(){}") {
			return fmt.Errorf("cache %q: path %q contains unsupported characters", c.Key, p)
		}
		if strings.Contains(p, "..") {
			return fmt.Errorf("cache %q: path %q must not contain '..'", c.Key, p)
		}
		if seen[p] {
			return fmt.Errorf("cache %q: duplicate path %q", c.Key, p)
		}
		seen[p] = true
	}
	return nil
}

func ParseCacheEntries(raw string) ([]CacheEntry, error) {
	var workflow struct {
		Cache []CacheEntry `yaml:"cache"`
	}
	if err := yaml.Unmarshal([]byte(raw), &workflow); err != nil {
		return nil, err
	}
	if err := ValidateCacheEntries(workflow.Cache); err != nil {
		return nil, err
	}
	return workflow.Cache, nil
}

func ValidateCacheEntries(entries []CacheEntry) error {
	seen := make(map[string]struct{}, len(entries))
	for _, entry := range entries {
		if err := entry.Validate(); err != nil {
			return err
		}
		if _, ok := seen[entry.Key]; ok {
			return fmt.Errorf("cache: duplicate key %q", entry.Key)
		}
		seen[entry.Key] = struct{}{}
	}
	return nil
}