| 102 | } |
| 103 | |
| 104 | func (tp *templateProcessor) Process(path string, file http.File, data any) ([]byte, error) { |
| 105 | stat, e := file.Stat() |
| 106 | if e != nil { |
| 107 | return nil, e |
| 108 | } |
| 109 | |
| 110 | if stat.IsDir() || (tp.filter != nil && !tp.filter(path)) { |
| 111 | return nil, ErrUnprocessed |
| 112 | } |
| 113 | |
| 114 | name := stat.Name() |
| 115 | tag := strconv.FormatInt(stat.ModTime().UnixMilli(), 10) + strconv.FormatInt(stat.Size(), 10) |
| 116 | |
| 117 | cached, ok := tp.cache.Get(name) |
| 118 | if !ok { |
| 119 | nc := newTemplateCache(name) |
| 120 | // SetIfAbsent avoids two concurrent first-requests creating |
| 121 | // (and parsing into) two different templateCache instances. |
| 122 | if tp.cache.SetIfAbsent(name, nc) { |
| 123 | cached = nc |
| 124 | } else { |
| 125 | cached, _ = tp.cache.Get(name) |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | // Hold the lock while reading/(re)parsing the template and capturing the |
| 130 | // parsed template pointer, to avoid a data race between Parse (write) and |
| 131 | // Execute (read) when the underlying file changes. |
| 132 | cached.l.Lock() |
| 133 | if tag != cached.tag { |
| 134 | content, e := io.ReadAll(file) |
| 135 | if e != nil { |
| 136 | cached.l.Unlock() |
| 137 | return nil, e |
| 138 | } |
| 139 | if e := cached.Parse(string(content)); e != nil { |
| 140 | cached.l.Unlock() |
| 141 | return nil, e |
| 142 | } |
| 143 | cached.tag = tag |
| 144 | } |
| 145 | t := cached.t |
| 146 | cached.l.Unlock() |
| 147 | |
| 148 | buf := bytes.NewBuffer(nil) |
| 149 | if e := t.Execute(buf, data); e != nil { |
| 150 | return nil, e |
| 151 | } |
| 152 | return buf.Bytes(), nil |
| 153 | } |
| 154 | |
| 155 | func newTemplateCache(name string) *templateCache { |
| 156 | return &templateCache{name: strings.ToLower(name), l: sync.Mutex{}} |