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 }