Files

114 lines
2.5 KiB
Go

package main
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"time"
)
const (
cacheBytesLimit = int64(16 << 20)
cacheFilesLimit = 1024
cacheScanLimit = 4096
cacheGrace = 30 * time.Second
)
var errScanLimit = errors.New("cache scan limit reached")
type cacheFile struct {
path string
size int64
mod time.Time
info fs.FileInfo
}
// cleanCache operates only in the dedicated Soloist cache directory. Recent files
// are left alone to avoid interfering with downloads still being written.
func cleanCache(root string, now time.Time) error {
info, err := os.Lstat(root)
if err != nil {
return err
}
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("cache root must be a directory, not a symlink")
}
var files []cacheFile
var total int64
scanned := 0
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
scanned++
if scanned > cacheScanLimit {
return errScanLimit
}
if !d.Type().IsRegular() {
return nil
}
i, err := d.Info()
if err != nil {
return err
}
if !i.Mode().IsRegular() {
return nil
}
files = append(files, cacheFile{path, i.Size(), i.ModTime(), i})
total += i.Size()
return nil
})
if err != nil && !errors.Is(err, errScanLimit) {
return err
}
sort.Slice(files, func(i, j int) bool { return files[i].mod.Before(files[j].mod) })
count := len(files)
for _, f := range files {
if total <= cacheBytesLimit && count <= cacheFilesLimit && !errors.Is(err, errScanLimit) {
break
}
if now.Sub(f.mod) < cacheGrace {
continue
}
// Do not follow symlinks or delete a file that changed during the scan.
current, e := os.Lstat(f.path)
if e != nil {
if errors.Is(e, os.ErrNotExist) {
continue
}
return e
}
if !current.Mode().IsRegular() || !os.SameFile(f.info, current) || current.Size() != f.size || !current.ModTime().Equal(f.mod) {
continue
}
if e = os.Remove(f.path); e != nil && !errors.Is(e, os.ErrNotExist) {
return e
}
total -= f.size
count--
// When truncated, clear enough entries for the next traversal to advance.
if errors.Is(err, errScanLimit) && count <= cacheFilesLimit {
break
}
}
if errors.Is(err, errScanLimit) {
return fmt.Errorf("%w; will retry", err)
}
return nil
}
func runCacheJanitor(root string) error {
if err := os.MkdirAll(root, 0700); err != nil {
return err
}
for {
if err := cleanCache(root, time.Now()); err != nil {
fmt.Fprintln(os.Stderr, "spotify cache:", err)
}
time.Sleep(3 * time.Second)
}
}