]> git.giacomolli.uk - onechat/blob - journal.go
Build the nodes from a queue in the background
[onechat] / journal.go
1 package main
2
3 import (
4         "bufio"
5         "context"
6         "encoding/json"
7         "errors"
8         "fmt"
9         "os"
10         "path/filepath"
11 )
12
13 // journal appends JSON lines to one file for each day in dir, named
14 // YYYY-MM-DD.jsonl. It keeps the file of the last day open.
15 type journal struct {
16         dir  string
17         file *os.File
18         day  string
19 }
20
21 // Write appends line to the file of day and flushes it to the disk.
22 func (j *journal) Write(day string, line []byte) error {
23         if day != j.day {
24                 err := j.Close()
25                 if err != nil {
26                         return err
27                 }
28
29                 j.file, err = os.OpenFile(filepath.Join(j.dir, day+".jsonl"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
30                 if err != nil {
31                         return fmt.Errorf("open day file: %w", err)
32                 }
33
34                 j.day = day
35         }
36
37         _, err := j.file.Write(line)
38         if err != nil {
39                 return fmt.Errorf("write line: %w", err)
40         }
41
42         err = j.file.Sync()
43         if err != nil {
44                 return fmt.Errorf("flush line: %w", err)
45         }
46
47         return nil
48 }
49
50 // Close closes the open day file.
51 func (j *journal) Close() error {
52         if j.file == nil {
53                 return nil
54         }
55
56         err := j.file.Close()
57         j.file = nil
58         j.day = ""
59         if err != nil {
60                 return fmt.Errorf("close day file: %w", err)
61         }
62
63         return nil
64 }
65
66 // readLines decodes each line of the day files in dir, oldest day first, and
67 // gives it to add in file order.
68 func readLines[T any](ctx context.Context, dir string, add func(T) error) error {
69         paths, err := filepath.Glob(filepath.Join(dir, "*.jsonl"))
70         if err != nil {
71                 return fmt.Errorf("list day files: %w", err)
72         }
73
74         for _, path := range paths {
75                 err = readFile(ctx, path, add)
76                 if err != nil {
77                         return err
78                 }
79         }
80
81         return nil
82 }
83
84 func readFile[T any](ctx context.Context, path string, add func(T) error) (err error) {
85         file, err := os.Open(path)
86         if err != nil {
87                 return fmt.Errorf("open day file: %w", err)
88         }
89
90         defer func() {
91                 err = errors.Join(err, file.Close())
92         }()
93
94         scanner := bufio.NewScanner(file)
95         scanner.Buffer(nil, 1<<30)
96         for number := 1; scanner.Scan(); number++ {
97                 if ctx.Err() != nil {
98                         return ctx.Err()
99                 }
100
101                 var value T
102                 err = json.Unmarshal(scanner.Bytes(), &value)
103                 if err == nil {
104                         err = add(value)
105                 }
106
107                 if err != nil {
108                         return fmt.Errorf("%s line %d: %w", filepath.Base(path), number, err)
109                 }
110         }
111
112         err = scanner.Err()
113         if err != nil {
114                 return fmt.Errorf("read %s: %w", filepath.Base(path), err)
115         }
116
117         return nil
118 }