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 }