Initial GoWallpaper implementation
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user