You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
noxy/cache.go

218 lines
5.3 KiB
Go

package noxy
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
"io/ioutil"
"mime"
"os"
"path/filepath"
)
var ErrCacheMiss = errors.New("cache miss")
// DataStream is an io.Reader augmented with a mime type.
type DataStream struct {
ContentType string
r io.Reader
}
// MimeType reports the mime type as parsed from ds.ContentType, ignoring
// any errors resulting from parsing optional media parameters.
func (ds DataStream) MimeType() string {
mtype, _, err := mime.ParseMediaType(ds.ContentType)
if err != nil && errors.Is(err, mime.ErrInvalidMediaParameter) {
return ds.ContentType
}
return mtype
}
func (ds DataStream) Read(p []byte) (n int, err error) {
return ds.r.Read(p)
}
func (ds DataStream) Close() error {
if closer, ok := ds.r.(io.Closer); ok {
return closer.Close()
}
return nil
}
// Cacher is used by Noxer to store and use meta info in JSON format, and stream files.
type Cacher interface {
GetJSON(ctx context.Context, key CacheKey, dst any) error
PutJSON(ctx context.Context, key CacheKey, v any) error
GetStream(ctx context.Context, key CacheKey) (*DataStream, error)
PutStream(ctx context.Context, key CacheKey, mimeType string, r io.Reader) error
}
// CacheKeyType allows CacheKey to segregate data based on their logical types.
type CacheKeyType byte
const (
_ CacheKeyType = 1 << iota
CacheKeyEvent // nostr event links
CacheKeyURLPreview // OG link preview metadata
CacheKeyData // actual url data
)
// CacheKey is 33 bytes long, encoding its CacheKeyType at index 32.
type CacheKey []byte
// MakeCacheKey creates a new cache key based on sha256 of s and the logical data type.
func MakeCacheKey(s string, typ CacheKeyType) CacheKey {
h := sha256.Sum256([]byte(s))
return append(h[:], byte(typ))
}
// String returns a key representation without its CacheKeyType.
func (k CacheKey) String() string {
return hex.EncodeToString(k[:len(k)-1])
}
// Path result is suitable as a filesystem path for storing a cache entry.
func (k CacheKey) Path() string {
h := hex.EncodeToString(k[:len(k)-1])
return filepath.Join(k.Namespace(), h[0:4], h)
}
// Namespace is a string representation of the key's CacheKeyType.
func (k CacheKey) Namespace() string {
typ := byte(0)
if n := len(k); n > 0 {
typ = k[n-1]
}
switch CacheKeyType(typ) {
default:
return ""
case CacheKeyEvent:
return "event"
case CacheKeyURLPreview:
return "preview"
case CacheKeyData:
return "data"
}
}
// DirCache implements Cacher using regular filesystem operations.
// it places all data under subdirectories of the Root.
//
// TODO: cap at max storage size
// TODO: max size per key
type DirCache struct {
Root string
}
const dirCacheMetaSuffix = ".meta.json"
func (d DirCache) makeFilepath(key CacheKey, mkdir bool) string {
p := filepath.Join(d.Root, key.Path())
if mkdir {
os.MkdirAll(filepath.Dir(p), 0700)
}
return p
}
func (d DirCache) makeTemp(key CacheKey) string {
p := filepath.Join(d.Root, "tmp", key.Path())
os.MkdirAll(filepath.Dir(p), 0700)
return p
}
func (d DirCache) GetJSON(ctx context.Context, key CacheKey, dst any) error {
b, err := os.ReadFile(d.makeFilepath(key, false))
switch {
case err != nil && errors.Is(err, os.ErrNotExist):
return ErrCacheMiss
case err != nil:
return err
}
return json.Unmarshal(b, dst)
}
func (d DirCache) PutJSON(ctx context.Context, key CacheKey, v any) error {
b, err := json.Marshal(v)
if err != nil {
return err
}
return ioutil.WriteFile(d.makeFilepath(key, true), b, 0600)
}
func (d DirCache) GetStream(ctx context.Context, key CacheKey) (*DataStream, error) {
filepath := d.makeFilepath(key, false)
f, err := os.Open(filepath)
switch {
case err != nil && errors.Is(err, os.ErrNotExist):
return nil, ErrCacheMiss
case err != nil:
return nil, err
}
mb, err := os.ReadFile(filepath + dirCacheMetaSuffix)
if err != nil {
f.Close()
return nil, err
}
var ds DataStream
if err := json.Unmarshal(mb, &ds); err != nil {
f.Close()
return nil, err
}
ds.r = f
return &ds, nil
}
func (d DirCache) PutStream(ctx context.Context, key CacheKey, mimeType string, r io.Reader) error {
ds := DataStream{ContentType: mimeType}
mb, err := json.Marshal(ds)
if err != nil {
return err
}
tmpfile := d.makeTemp(key)
tmpmeta := tmpfile + dirCacheMetaSuffix
if err := ioutil.WriteFile(tmpmeta, mb, 0600); err != nil {
return err
}
f, err := os.OpenFile(tmpfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
if _, err := io.Copy(f, r); err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
filepath := d.makeFilepath(key, true)
if err := os.Rename(tmpfile, filepath); err != nil {
return err
}
return os.Rename(tmpmeta, filepath+dirCacheMetaSuffix)
}
// NullCache stores no data.
var NullCache nullCache
type nullCache struct{}
func (nc nullCache) GetJSON(context.Context, CacheKey, any) error {
return ErrCacheMiss
}
func (nc nullCache) PutJSON(context.Context, CacheKey, any) error {
return nil
}
func (nc nullCache) GetStream(context.Context, CacheKey) (*DataStream, error) {
return nil, ErrCacheMiss
}
func (nc nullCache) PutStream(ctx context.Context, k CacheKey, mtype string, r io.Reader) error {
_, err := io.Copy(io.Discard, r)
return err
}