114 lines
2.5 KiB
Go
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)
|
|
}
|
|
}
|