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.
218 lines
5.3 KiB
Go
218 lines
5.3 KiB
Go
2 years ago
|
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
|
||
|
}
|