| 1 | package assetcache |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "fmt" |
| 6 | "math" |
| 7 | "net/http" |
| 8 | "time" |
| 9 | ) |
| 10 | |
| 11 | // Meta is what the store keeps beside an entry's body files. GzipSize is |
| 12 | // zero when there is no gzip copy. Version names the body files, so a |
| 13 | // commit never overwrites bytes an older Meta still points at. |
| 14 | type Meta struct { |
| 15 | Key Key |
| 16 | Version string |
| 17 | Header http.Header |
| 18 | Lifetime time.Duration |
| 19 | Expires time.Time |
| 20 | StoredAt time.Time |
| 21 | Size int64 |
| 22 | GzipSize int64 |
| 23 | } |
| 24 | |
| 25 | // Age is how long ago, by its lifetime, the entry was fresh from the origin: |
| 26 | // Lifetime minus the time left until Expires. It is never below zero, so an |
| 27 | // entry stored "in the future" before a clock step reports age zero. |
| 28 | func (m Meta) Age(now time.Time) time.Duration { |
| 29 | if a := m.Lifetime - m.Expires.Sub(now); a > 0 { |
| 30 | return a |
| 31 | } |
| 32 | return 0 |
| 33 | } |
| 34 | |
| 35 | func encodeMeta(m Meta) ([]byte, error) { |
| 36 | return json.Marshal(m) |
| 37 | } |
| 38 | |
| 39 | // decodeMeta parses a .meta file. Fields it does not know are ignored, so a |
| 40 | // file written by a newer binary still loads. |
| 41 | func decodeMeta(raw []byte) (Meta, error) { |
| 42 | var m Meta |
| 43 | if err := json.Unmarshal(raw, &m); err != nil { |
| 44 | return Meta{}, err |
| 45 | } |
| 46 | if !validVersion(m.Version) { |
| 47 | return Meta{}, fmt.Errorf("version %q is not a file name part", m.Version) |
| 48 | } |
| 49 | if m.Size < 0 || m.GzipSize < 0 || m.Size > math.MaxInt64-m.GzipSize { |
| 50 | return Meta{}, fmt.Errorf("sizes %d and %d are not usable", m.Size, m.GzipSize) |
| 51 | } |
| 52 | return m, nil |
| 53 | } |
| 54 | |
| 55 | // validVersion accepts the short alphanumeric tokens the store draws, and |
| 56 | // nothing that could change which file a name refers to. |
| 57 | func validVersion(v string) bool { |
| 58 | if v == "" || len(v) > 64 { |
| 59 | return false |
| 60 | } |
| 61 | for _, c := range v { |
| 62 | switch { |
| 63 | case c >= '0' && c <= '9', c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z': |
| 64 | default: |
| 65 | return false |
| 66 | } |
| 67 | } |
| 68 | return true |
| 69 | } |
| 70 | |
| 71 | func metaName(id string) string { return id + ".meta" } |
| 72 | func bodyName(id, version string) string { return id + "." + version + ".body" } |
| 73 | func gzipName(id, version string) string { return id + "." + version + ".gz" } |
| 74 | |