~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
216 lines · 4.6 KB Raw History
package plc

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"sort"
	"sync"
	"time"
)

type Store interface {
	GetCurrentOps(ctx context.Context, did string) ([]LogEntry, error)
	GetAuditLog(ctx context.Context, did string) ([]LogEntry, error)
	GetOpByCID(ctx context.Context, did string, cid string) (*LogEntry, error)
	InsertOp(ctx context.Context, entry LogEntry) error
	NullifyOps(ctx context.Context, did string, cids []string) error
	ExportLogs(ctx context.Context, after *time.Time, count int) ([]LogEntry, error)
}

type MemoryStore struct {
	mu   sync.RWMutex
	logs map[string][]LogEntry
}

func NewMemoryStore() *MemoryStore {
	return &MemoryStore{logs: map[string][]LogEntry{}}
}

var (
	_ Store = (*MemoryStore)(nil)

	_ Store = (*FileStore)(nil)
)

func OpenStore(path string) (Store, error) {
	if path == "" {
		return NewMemoryStore(), nil
	}
	fs, err := NewFileStore(path)
	if err != nil {
		return nil, err
	}
	return fs, nil
}

func (s *MemoryStore) GetCurrentOps(ctx context.Context, did string) ([]LogEntry, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := []LogEntry{}
	for _, e := range s.logs[did] {
		if !e.Nullified {
			out = append(out, e)
		}
	}
	return out, nil
}

func (s *MemoryStore) GetAuditLog(ctx context.Context, did string) ([]LogEntry, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := append([]LogEntry{}, s.logs[did]...)
	return out, nil
}

func (s *MemoryStore) GetOpByCID(ctx context.Context, did string, cid string) (*LogEntry, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for i := range s.logs[did] {
		if s.logs[did][i].CID == cid {
			e := s.logs[did][i]
			return &e, nil
		}
	}
	return nil, nil
}

func (s *MemoryStore) InsertOp(ctx context.Context, entry LogEntry) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.logs[entry.DID] = append(s.logs[entry.DID], entry)
	return nil
}

func (s *MemoryStore) NullifyOps(ctx context.Context, did string, cids []string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	set := make(map[string]struct{}, len(cids))
	for _, c := range cids {
		set[c] = struct{}{}
	}
	for i := range s.logs[did] {
		if _, ok := set[s.logs[did][i].CID]; ok {
			s.logs[did][i].Nullified = true
		}
	}
	return nil
}

func (s *MemoryStore) ExportLogs(ctx context.Context, after *time.Time, count int) ([]LogEntry, error) {
	s.mu.RLock()
	all := []LogEntry{}
	for _, entries := range s.logs {
		all = append(all, entries...)
	}
	s.mu.RUnlock()
	if after != nil {
		filtered := all[:0]
		for _, e := range all {
			t, err := time.Parse(time.RFC3339Nano, e.CreatedAt)
			if err != nil || t.After(*after) {
				filtered = append(filtered, e)
			}
		}
		all = filtered
	}
	sort.SliceStable(all, func(i, j int) bool { return all[i].CreatedAt < all[j].CreatedAt })
	if count >= 0 && len(all) > count {
		all = all[:count]
	}
	return all, nil
}

func (s *MemoryStore) snapshot() map[string][]LogEntry {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make(map[string][]LogEntry, len(s.logs))
	for did, entries := range s.logs {
		out[did] = append([]LogEntry{}, entries...)
	}
	return out
}

func (s *MemoryStore) replace(logs map[string][]LogEntry) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.logs = logs
}

type FileStore struct {
	*MemoryStore
	path string
	mu   sync.Mutex
}

func NewFileStore(path string) (*FileStore, error) {
	fs := &FileStore{MemoryStore: NewMemoryStore(), path: path}
	b, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return fs, nil
		}
		return nil, err
	}
	if len(b) == 0 {
		return fs, nil
	}
	var logs map[string][]LogEntry
	if err := json.Unmarshal(b, &logs); err != nil {
		return nil, err
	}
	if logs == nil {
		logs = map[string][]LogEntry{}
	}
	fs.MemoryStore.replace(logs)
	return fs, nil
}

func (s *FileStore) InsertOp(ctx context.Context, entry LogEntry) error {
	if err := s.MemoryStore.InsertOp(ctx, entry); err != nil {
		return err
	}
	return s.save()
}

func (s *FileStore) NullifyOps(ctx context.Context, did string, cids []string) error {
	if err := s.MemoryStore.NullifyOps(ctx, did, cids); err != nil {
		return err
	}
	return s.save()
}

func (s *FileStore) save() error {
	s.mu.Lock()
	defer s.mu.Unlock()
	b, err := json.Marshal(s.MemoryStore.snapshot())
	if err != nil {
		return err
	}
	dir := filepath.Dir(s.path)
	if dir != "" && dir != "." {
		if err := os.MkdirAll(dir, 0o700); err != nil {
			return err
		}
	}
	tmp, err := os.CreateTemp(dir, filepath.Base(s.path)+".tmp-*")
	if err != nil {
		return err
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName)
	if err := tmp.Chmod(0o600); err != nil {
		tmp.Close()
		return err
	}
	if _, err := tmp.Write(b); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	return os.Rename(tmpName, s.path)
}