Files
schreifuchs.ch/internal/filesystem/cache.go
T
2026-03-02 22:38:01 +01:00

163 lines
3.8 KiB
Go

package filesystem
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"sync/atomic"
"time"
"github.com/valkey-io/valkey-go"
)
// CachedClient wraps a FileSystem implementation with Valkey caching.
type CachedClient struct {
impl FS
client valkey.Client
refreshTime time.Duration
hits atomic.Int64
misses atomic.Int64
readDirHits atomic.Int64
readHits atomic.Int64
}
// NewCachedClient creates a new CachedClient.
func NewCachedClient(impl FS, client valkey.Client, ttl time.Duration) *CachedClient {
return &CachedClient{
impl: impl,
client: client,
refreshTime: ttl,
}
}
func (c *CachedClient) ReadDir(path string) ([]os.FileInfo, error) {
key := fmt.Sprintf("webdav:readdir:%s", path)
ctx := context.Background()
// Try cache
val, err := c.client.Do(ctx, c.client.B().Get().Key(key).Build()).ToString()
if err == nil && val != "" {
var cached []File
if err := json.Unmarshal([]byte(val), &cached); err == nil {
c.readDirHits.Add(1)
c.hits.Add(1)
slog.Debug("cache hit", "key", key)
infos := make([]os.FileInfo, len(cached))
for i, f := range cached {
infos[i] = f
}
return infos, nil
}
}
// Cache miss
c.misses.Add(1)
slog.Debug("cache miss", "key", key)
infos, err := c.impl.ReadDir(path)
if err != nil {
return nil, err
}
// Serialize and cache
cached := make([]File, 0, len(infos))
for _, info := range infos {
// Type assert to ensure we capture ContentType and Path
if fi, ok := info.(FileInfo); ok {
cached = append(cached, File{
name: fi.Name(),
size: fi.Size(),
mode: fi.Mode(),
modTime: fi.ModTime(),
isDir: fi.IsDir(),
contentType: fi.ContentType(),
path: fi.Path(),
})
} else {
// Fallback if underlying impl returns generic os.FileInfo
cached = append(cached, File{
name: info.Name(),
size: info.Size(),
mode: info.Mode(),
modTime: info.ModTime(),
isDir: info.IsDir(),
})
}
}
bytes, err := json.Marshal(cached)
if err == nil {
c.client.Do(ctx, c.client.B().Set().Key(key).Value(string(bytes)).Ex(c.refreshTime).Build())
}
return infos, nil
}
func (c *CachedClient) Read(path string) ([]byte, error) {
key := fmt.Sprintf("webdav:read:%s", path)
ctx := context.Background()
// Try cache
val, err := c.client.Do(ctx, c.client.B().Get().Key(key).Build()).AsBytes()
if err == nil && len(val) > 0 {
c.readHits.Add(1)
c.hits.Add(1)
slog.Debug("cache hit", "key", key)
return val, nil
}
// Cache miss
c.misses.Add(1)
slog.Debug("cache miss", "key", key)
data, err := c.impl.Read(path)
if err != nil {
return nil, err
}
// Cache
c.client.Do(ctx, c.client.B().Set().Key(key).Value(valkey.BinaryString(data)).Ex(c.refreshTime).Build())
return data, nil
}
func (c *CachedClient) Stat(path string) (info os.FileInfo, err error) {
key := fmt.Sprintf("webdav:stat:%s", path)
ctx := context.Background()
// Try cache
val, err := c.client.Do(ctx, c.client.B().Get().Key(key).Build()).ToString()
if err == nil && val != "" {
if err := json.Unmarshal([]byte(val), &info); err == nil {
c.hits.Add(1)
slog.Debug("cache hit", "key", key)
return info, nil
}
}
// Cache miss
c.misses.Add(1)
slog.Debug("cache miss", "key", key)
fileInfo, err := c.impl.Stat(path)
if err != nil {
return
}
// Serialize and cache
// Type assert to ensure we capture ContentType and Path
info = ParseToFile(fileInfo)
bytes, err := json.Marshal(info)
if err == nil {
c.client.Do(ctx, c.client.B().Set().Key(key).Value(string(bytes)).Ex(c.refreshTime).Build())
}
return
}
func (c *CachedClient) Invalidate(path string) {
ctx := context.Background()
c.client.Do(ctx, c.client.B().Del().Key(fmt.Sprintf("webdav:readdir:%s", path)).Key(fmt.Sprintf("webdav:read:%s", path)).Build())
}