github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/cache/filecache/filecache.go (about) 1 // Copyright 2018 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package filecache 15 16 import ( 17 "bytes" 18 "errors" 19 "io" 20 "os" 21 "path/filepath" 22 "strings" 23 "sync" 24 "time" 25 26 "github.com/gohugoio/hugo/common/hugio" 27 28 "github.com/gohugoio/hugo/helpers" 29 30 "github.com/BurntSushi/locker" 31 "github.com/spf13/afero" 32 ) 33 34 // ErrFatal can be used to signal an unrecoverable error. 35 var ErrFatal = errors.New("fatal filecache error") 36 37 const ( 38 FilecacheRootDirname = "filecache" 39 ) 40 41 // Cache caches a set of files in a directory. This is usually a file on 42 // disk, but since this is backed by an Afero file system, it can be anything. 43 type Cache struct { 44 Fs afero.Fs 45 46 // Max age for items in this cache. Negative duration means forever, 47 // 0 is effectively turning this cache off. 48 maxAge time.Duration 49 50 // When set, we just remove this entire root directory on expiration. 51 pruneAllRootDir string 52 53 nlocker *lockTracker 54 55 initOnce sync.Once 56 initErr error 57 } 58 59 type lockTracker struct { 60 seenMu sync.RWMutex 61 seen map[string]struct{} 62 63 *locker.Locker 64 } 65 66 // Lock tracks the ids in use. We use this information to do garbage collection 67 // after a Hugo build. 68 func (l *lockTracker) Lock(id string) { 69 l.seenMu.RLock() 70 if _, seen := l.seen[id]; !seen { 71 l.seenMu.RUnlock() 72 l.seenMu.Lock() 73 l.seen[id] = struct{}{} 74 l.seenMu.Unlock() 75 } else { 76 l.seenMu.RUnlock() 77 } 78 79 l.Locker.Lock(id) 80 } 81 82 // ItemInfo contains info about a cached file. 83 type ItemInfo struct { 84 // This is the file's name relative to the cache's filesystem. 85 Name string 86 } 87 88 // NewCache creates a new file cache with the given filesystem and max age. 89 func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache { 90 return &Cache{ 91 Fs: fs, 92 nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})}, 93 maxAge: maxAge, 94 pruneAllRootDir: pruneAllRootDir, 95 } 96 } 97 98 // lockedFile is a file with a lock that is released on Close. 99 type lockedFile struct { 100 afero.File 101 unlock func() 102 } 103 104 func (l *lockedFile) Close() error { 105 defer l.unlock() 106 return l.File.Close() 107 } 108 109 func (c *Cache) init() error { 110 c.initOnce.Do(func() { 111 // Create the base dir if it does not exist. 112 if err := c.Fs.MkdirAll("", 0777); err != nil && !os.IsExist(err) { 113 c.initErr = err 114 } 115 }) 116 return c.initErr 117 } 118 119 // WriteCloser returns a transactional writer into the cache. 120 // It's important that it's closed when done. 121 func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) { 122 if err := c.init(); err != nil { 123 return ItemInfo{}, nil, err 124 } 125 126 id = cleanID(id) 127 c.nlocker.Lock(id) 128 129 info := ItemInfo{Name: id} 130 131 f, err := helpers.OpenFileForWriting(c.Fs, id) 132 if err != nil { 133 c.nlocker.Unlock(id) 134 return info, nil, err 135 } 136 137 return info, &lockedFile{ 138 File: f, 139 unlock: func() { c.nlocker.Unlock(id) }, 140 }, nil 141 } 142 143 // ReadOrCreate tries to lookup the file in cache. 144 // If found, it is passed to read and then closed. 145 // If not found a new file is created and passed to create, which should close 146 // it when done. 147 func (c *Cache) ReadOrCreate(id string, 148 read func(info ItemInfo, r io.ReadSeeker) error, 149 create func(info ItemInfo, w io.WriteCloser) error) (info ItemInfo, err error) { 150 if err := c.init(); err != nil { 151 return ItemInfo{}, err 152 } 153 154 id = cleanID(id) 155 156 c.nlocker.Lock(id) 157 defer c.nlocker.Unlock(id) 158 159 info = ItemInfo{Name: id} 160 161 if r := c.getOrRemove(id); r != nil { 162 err = read(info, r) 163 defer r.Close() 164 if err == nil || err == ErrFatal { 165 // See https://github.com/gohugoio/hugo/issues/6401 166 // To recover from file corruption we handle read errors 167 // as the cache item was not found. 168 // Any file permission issue will also fail in the next step. 169 return 170 } 171 } 172 173 f, err := helpers.OpenFileForWriting(c.Fs, id) 174 if err != nil { 175 return 176 } 177 178 err = create(info, f) 179 180 return 181 } 182 183 // GetOrCreate tries to get the file with the given id from cache. If not found or expired, create will 184 // be invoked and the result cached. 185 // This method is protected by a named lock using the given id as identifier. 186 func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (ItemInfo, io.ReadCloser, error) { 187 if err := c.init(); err != nil { 188 return ItemInfo{}, nil, err 189 } 190 id = cleanID(id) 191 192 c.nlocker.Lock(id) 193 defer c.nlocker.Unlock(id) 194 195 info := ItemInfo{Name: id} 196 197 if r := c.getOrRemove(id); r != nil { 198 return info, r, nil 199 } 200 201 var ( 202 r io.ReadCloser 203 err error 204 ) 205 206 r, err = create() 207 if err != nil { 208 return info, nil, err 209 } 210 211 if c.maxAge == 0 { 212 // No caching. 213 return info, hugio.ToReadCloser(r), nil 214 } 215 216 var buff bytes.Buffer 217 return info, 218 hugio.ToReadCloser(&buff), 219 afero.WriteReader(c.Fs, id, io.TeeReader(r, &buff)) 220 } 221 222 // GetOrCreateBytes is the same as GetOrCreate, but produces a byte slice. 223 func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (ItemInfo, []byte, error) { 224 if err := c.init(); err != nil { 225 return ItemInfo{}, nil, err 226 } 227 id = cleanID(id) 228 229 c.nlocker.Lock(id) 230 defer c.nlocker.Unlock(id) 231 232 info := ItemInfo{Name: id} 233 234 if r := c.getOrRemove(id); r != nil { 235 defer r.Close() 236 b, err := io.ReadAll(r) 237 return info, b, err 238 } 239 240 var ( 241 b []byte 242 err error 243 ) 244 245 b, err = create() 246 if err != nil { 247 return info, nil, err 248 } 249 250 if c.maxAge == 0 { 251 return info, b, nil 252 } 253 254 if err := afero.WriteReader(c.Fs, id, bytes.NewReader(b)); err != nil { 255 return info, nil, err 256 } 257 return info, b, nil 258 } 259 260 // GetBytes gets the file content with the given id from the cache, nil if none found. 261 func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) { 262 if err := c.init(); err != nil { 263 return ItemInfo{}, nil, err 264 } 265 id = cleanID(id) 266 267 c.nlocker.Lock(id) 268 defer c.nlocker.Unlock(id) 269 270 info := ItemInfo{Name: id} 271 272 if r := c.getOrRemove(id); r != nil { 273 defer r.Close() 274 b, err := io.ReadAll(r) 275 return info, b, err 276 } 277 278 return info, nil, nil 279 } 280 281 // Get gets the file with the given id from the cache, nil if none found. 282 func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) { 283 if err := c.init(); err != nil { 284 return ItemInfo{}, nil, err 285 } 286 id = cleanID(id) 287 288 c.nlocker.Lock(id) 289 defer c.nlocker.Unlock(id) 290 291 info := ItemInfo{Name: id} 292 293 r := c.getOrRemove(id) 294 295 return info, r, nil 296 } 297 298 // getOrRemove gets the file with the given id. If it's expired, it will 299 // be removed. 300 func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser { 301 if c.maxAge == 0 { 302 // No caching. 303 return nil 304 } 305 306 if c.maxAge > 0 { 307 fi, err := c.Fs.Stat(id) 308 if err != nil { 309 return nil 310 } 311 312 if c.isExpired(fi.ModTime()) { 313 c.Fs.Remove(id) 314 return nil 315 } 316 } 317 318 f, err := c.Fs.Open(id) 319 if err != nil { 320 return nil 321 } 322 323 return f 324 } 325 326 func (c *Cache) isExpired(modTime time.Time) bool { 327 if c.maxAge < 0 { 328 return false 329 } 330 331 // Note the use of time.Since here. 332 // We cannot use Hugo's global Clock for this. 333 return c.maxAge == 0 || time.Since(modTime) > c.maxAge 334 } 335 336 // For testing 337 func (c *Cache) GetString(id string) string { 338 id = cleanID(id) 339 340 c.nlocker.Lock(id) 341 defer c.nlocker.Unlock(id) 342 343 f, err := c.Fs.Open(id) 344 if err != nil { 345 return "" 346 } 347 defer f.Close() 348 349 b, _ := io.ReadAll(f) 350 return string(b) 351 } 352 353 // Caches is a named set of caches. 354 type Caches map[string]*Cache 355 356 // Get gets a named cache, nil if none found. 357 func (f Caches) Get(name string) *Cache { 358 return f[strings.ToLower(name)] 359 } 360 361 // NewCaches creates a new set of file caches from the given 362 // configuration. 363 func NewCaches(p *helpers.PathSpec) (Caches, error) { 364 dcfg := p.Cfg.GetConfigSection("caches").(Configs) 365 fs := p.Fs.Source 366 367 m := make(Caches) 368 for k, v := range dcfg { 369 var cfs afero.Fs 370 371 if v.IsResourceDir { 372 cfs = p.BaseFs.ResourcesCache 373 } else { 374 cfs = fs 375 } 376 377 if cfs == nil { 378 panic("nil fs") 379 } 380 381 baseDir := v.DirCompiled 382 383 bfs := afero.NewBasePathFs(cfs, baseDir) 384 385 var pruneAllRootDir string 386 if k == CacheKeyModules { 387 pruneAllRootDir = "pkg" 388 } 389 390 m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir) 391 } 392 393 return m, nil 394 } 395 396 func cleanID(name string) string { 397 return strings.TrimPrefix(filepath.Clean(name), helpers.FilePathSeparator) 398 }