package assetcache import ( "encoding/json" "fmt" "math" "net/http" "time" ) // Meta is what the store keeps beside an entry's body files. GzipSize is // zero when there is no gzip copy. Version names the body files, so a // commit never overwrites bytes an older Meta still points at. type Meta struct { Key Key Version string Header http.Header Lifetime time.Duration Expires time.Time StoredAt time.Time Size int64 GzipSize int64 } // Age is how long ago, by its lifetime, the entry was fresh from the origin: // Lifetime minus the time left until Expires. It is never below zero, so an // entry stored "in the future" before a clock step reports age zero. func (m Meta) Age(now time.Time) time.Duration { if a := m.Lifetime - m.Expires.Sub(now); a > 0 { return a } return 0 } func encodeMeta(m Meta) ([]byte, error) { return json.Marshal(m) } // decodeMeta parses a .meta file. Fields it does not know are ignored, so a // file written by a newer binary still loads. func decodeMeta(raw []byte) (Meta, error) { var m Meta if err := json.Unmarshal(raw, &m); err != nil { return Meta{}, err } if !validVersion(m.Version) { return Meta{}, fmt.Errorf("version %q is not a file name part", m.Version) } if m.Size < 0 || m.GzipSize < 0 || m.Size > math.MaxInt64-m.GzipSize { return Meta{}, fmt.Errorf("sizes %d and %d are not usable", m.Size, m.GzipSize) } return m, nil } // validVersion accepts the short alphanumeric tokens the store draws, and // nothing that could change which file a name refers to. func validVersion(v string) bool { if v == "" || len(v) > 64 { return false } for _, c := range v { switch { case c >= '0' && c <= '9', c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z': default: return false } } return true } func metaName(id string) string { return id + ".meta" } func bodyName(id, version string) string { return id + "." + version + ".body" } func gzipName(id, version string) string { return id + "." + version + ".gz" }