Files
gowallpaper/cache.go
T

178 lines
4.6 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"time"
)
const cacheIndexFileName = "index.json"
type CacheEntry struct {
FileName string `json:"file_name"`
CreatedAt time.Time `json:"created_at"`
}
type CacheIndex struct {
Entries []CacheEntry `json:"entries"`
Current int `json:"current"`
cacheDir string
}
func loadCacheIndex(cacheDir string) (*CacheIndex, error) {
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
return nil, fmt.Errorf("创建缓存目录: %w", err)
}
idx := &CacheIndex{Current: -1, cacheDir: cacheDir}
path := filepath.Join(cacheDir, cacheIndexFileName)
data, err := os.ReadFile(path)
if err == nil {
if err := json.Unmarshal(data, idx); err != nil {
return nil, fmt.Errorf("解析缓存索引: %w", err)
}
} else if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("读取缓存索引: %w", err)
}
idx.cacheDir = cacheDir
changed := idx.repair()
if errors.Is(err, os.ErrNotExist) || changed {
if err := idx.save(); err != nil {
return nil, err
}
}
return idx, nil
}
func (idx *CacheIndex) repair() bool {
changed := false
valid := make([]CacheEntry, 0, len(idx.Entries))
seen := make(map[string]bool)
for _, entry := range idx.Entries {
if entry.FileName == "" || filepath.Base(entry.FileName) != entry.FileName || seen[entry.FileName] {
changed = true
continue
}
if _, err := os.Stat(filepath.Join(idx.cacheDir, entry.FileName)); err != nil {
changed = true
continue
}
seen[entry.FileName] = true
valid = append(valid, entry)
}
idx.Entries = valid
if len(idx.Entries) == 0 {
if idx.Current != -1 {
changed = true
}
idx.Current = -1
} else if idx.Current < 0 || idx.Current >= len(idx.Entries) {
idx.Current = len(idx.Entries) - 1
changed = true
}
return changed
}
func (idx *CacheIndex) save() error {
data, err := json.MarshalIndent(struct {
Entries []CacheEntry `json:"entries"`
Current int `json:"current"`
}{idx.Entries, idx.Current}, "", " ")
if err != nil {
return err
}
data = append(data, '\n')
if err := atomicWriteFile(filepath.Join(idx.cacheDir, cacheIndexFileName), data, 0o644); err != nil {
return fmt.Errorf("保存缓存索引: %w", err)
}
return nil
}
func (idx *CacheIndex) add(path string, maxCount int) error {
oldEntries := append([]CacheEntry(nil), idx.Entries...)
oldCurrent := idx.Current
name := filepath.Base(path)
idx.Entries = append(idx.Entries, CacheEntry{FileName: name, CreatedAt: time.Now()})
idx.Current = len(idx.Entries) - 1
if err := idx.trim(maxCount); err != nil {
idx.Entries = oldEntries
idx.Current = oldCurrent
return err
}
return nil
}
func (idx *CacheIndex) trim(maxCount int) error {
if maxCount <= 0 || len(idx.Entries) <= maxCount {
return idx.save()
}
oldEntries := append([]CacheEntry(nil), idx.Entries...)
oldCurrent := idx.Current
removeCount := len(idx.Entries) - maxCount
removed := append([]CacheEntry(nil), idx.Entries[:removeCount]...)
idx.Entries = append([]CacheEntry(nil), idx.Entries[removeCount:]...)
idx.Current -= removeCount
if idx.Current < 0 && len(idx.Entries) > 0 {
idx.Current = 0
}
if err := idx.save(); err != nil {
idx.Entries = oldEntries
idx.Current = oldCurrent
return err
}
for _, entry := range removed {
_ = os.Remove(filepath.Join(idx.cacheDir, entry.FileName))
}
return nil
}
func (idx *CacheIndex) previous() (string, bool, error) {
if idx.Current <= 0 || len(idx.Entries) == 0 {
return "", false, nil
}
idx.Current--
if err := idx.save(); err != nil {
idx.Current++
return "", false, err
}
return filepath.Join(idx.cacheDir, idx.Entries[idx.Current].FileName), true, nil
}
func (idx *CacheIndex) next() (string, bool, error) {
if idx.Current < 0 || idx.Current >= len(idx.Entries)-1 {
return "", false, nil
}
idx.Current++
if err := idx.save(); err != nil {
idx.Current--
return "", false, err
}
return filepath.Join(idx.cacheDir, idx.Entries[idx.Current].FileName), true, nil
}
func uniqueCachePath(cacheDir string) string {
base := time.Now().Format("20060102_150405_000000000")
path := filepath.Join(cacheDir, base+".jpg")
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return path
}
for i := 1; ; i++ {
candidate := filepath.Join(cacheDir, fmt.Sprintf("%s_%d.jpg", base, i))
if _, err := os.Stat(candidate); errors.Is(err, os.ErrNotExist) {
return candidate
}
}
}
func cachedImageFiles(cacheDir string) ([]string, error) {
matches, err := filepath.Glob(filepath.Join(cacheDir, "*.jpg"))
if err != nil {
return nil, err
}
sort.Strings(matches)
return matches, nil
}