clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / tinygit / journal.go
journal.go 3.6K
1package tinygit23import (4 "context"5 "crypto/sha256"6 "encoding/hex"7 "encoding/json"8 "errors"9 "fmt"10 "os"11 "path/filepath"12 "strings"13)1415type journalRecord struct {16 Repository string `json:"repository"`17 EventID string `json:"event_id"`18 Kind int `json:"kind"`19 Refs map[string]string `json:"refs,omitempty"`20 Head string `json:"head,omitempty"`21 Committed bool `json:"committed"`22}2324func (g *GitRelay) journalDir() string { return filepath.Join(g.root, ".tinyrelay", "journal") }2526func (g *GitRelay) journalPath(r journalRecord) string {27 h := sha256.Sum256([]byte(r.Repository + "\x00" + r.EventID))28 return filepath.Join(g.journalDir(), hex.EncodeToString(h[:])+".json")29}3031func (g *GitRelay) writeJournal(repo Repository, r journalRecord) error {32 if err := os.MkdirAll(g.journalDir(), 0700); err != nil {33 return err34 }35 b, err := json.Marshal(r)36 if err != nil {37 return err38 }39 tmp := g.journalPath(r) + ".tmp"40 if err := os.WriteFile(tmp, b, 0600); err != nil {41 return err42 }43 if err := syncFile(tmp); err != nil {44 return err45 }46 return os.Rename(tmp, g.journalPath(r))47}4849func (g *GitRelay) commitJournal(repo Repository, r journalRecord) error {50 if err := os.Remove(g.journalPath(r)); err != nil && !errors.Is(err, os.ErrNotExist) {51 return err52 }53 return nil54}5556func syncFile(path string) error {57 f, err := os.OpenFile(path, os.O_RDONLY, 0)58 if err != nil {59 return err60 }61 defer f.Close()62 return f.Sync()63}6465// recoverJournals completes state publication after a process crash. A66// journal is deliberately small and repository-local; the event store remains67// authoritative for event durability, while the file is the Git authority.68func (g *GitRelay) recoverJournals() error {69 entries, err := os.ReadDir(g.journalDir())70 if errors.Is(err, os.ErrNotExist) {71 return nil72 }73 if err != nil {74 return err75 }76 for _, entry := range entries {77 if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {78 continue79 }80 b, err := os.ReadFile(filepath.Join(g.journalDir(), entry.Name()))81 if err != nil {82 return err83 }84 var jr journalRecord85 if err := json.Unmarshal(b, &jr); err != nil {86 return fmt.Errorf("git relay journal %s: %w", entry.Name(), err)87 }88 owner, id, ok := strings.Cut(jr.Repository, "\x00")89 if !ok || owner == "" || id == "" {90 return fmt.Errorf("git relay journal %s: invalid repository", entry.Name())91 }92 r := Repository{Owner: owner, Identifier: id, Refs: jr.Refs, Head: jr.Head}93 if err := g.ensureRepo(r); err != nil {94 return err95 }96 keepJournal := false97 if jr.Kind == 30618 {98 current, found, err := g.store.Latest(context.Background(), 30618, owner, id)99 if err != nil {100 return err101 }102 if !found || current.ID != jr.EventID {103 if removeErr := os.Remove(filepath.Join(g.journalDir(), entry.Name())); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {104 return removeErr105 }106 continue107 }108 if g.stateObjectsPresent(context.Background(), r) {109 err = g.writeState(r)110 } else {111 err = g.writePendingState(r)112 keepJournal = true113 }114 if err != nil {115 return err116 }117 g.mu.Lock()118 g.pending[jr.EventID] = struct{}{}119 g.mu.Unlock()120 } else if jr.Kind == 1617 || jr.Kind == 1618 || jr.Kind == 1619 {121 if !g.stateObjectsPresent(context.Background(), r) {122 if err := g.writePendingState(r); err != nil {123 return err124 }125 keepJournal = true126 g.mu.Lock()127 g.pending[jr.EventID] = struct{}{}128 g.mu.Unlock()129 }130 }131 if !keepJournal {132 if err := os.Remove(filepath.Join(g.journalDir(), entry.Name())); err != nil && !errors.Is(err, os.ErrNotExist) {133 return err134 }135 }136 }137 return nil138}