]> git.giacomolli.uk - onechat/blob - journal.go
Name the reply kind for its role
[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 }