178 lines
4.6 KiB
Go
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
|
|
}
|