]> git.giacomolli.uk - onechat/blob - records.go
Share the request chain of a turn
[onechat] / records.go
1 package main
2
3 import (
4         "encoding/binary"
5         "errors"
6         "fmt"
7         "io"
8         "os"
9 )
10
11 // positionBytes is the size of an encoded position.
12 const positionBytes = 16
13
14 // openRecords opens or creates the record file at path with records of size
15 // bytes.
16 func openRecords(path string, size int) (*Records, error) {
17         file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
18         if err != nil {
19                 return nil, fmt.Errorf("open index: %w", err)
20         }
21
22         info, err := file.Stat()
23         if err != nil {
24                 return nil, errors.Join(fmt.Errorf("read index size: %w", err), file.Close())
25         }
26
27         r := &Records{
28                 file:  file,
29                 size:  size,
30                 count: int(info.Size()) / size,
31         }
32
33         if info.Size()%int64(size) != 0 {
34                 err = file.Truncate(int64(r.count * size))
35                 if err != nil {
36                         return nil, errors.Join(fmt.Errorf("cut partial index record: %w", err), file.Close())
37                 }
38         }
39
40         return r, nil
41 }
42
43 // encodePosition writes place into the first positionBytes bytes of record.
44 func encodePosition(record []byte, place Position) {
45         binary.LittleEndian.PutUint32(record[0:], place.day)
46         binary.LittleEndian.PutUint32(record[4:], place.length)
47         binary.LittleEndian.PutUint64(record[8:], place.offset)
48 }
49
50 // decodePosition reads a position from the first positionBytes bytes of record.
51 func decodePosition(record []byte) Position {
52         return Position{
53                 day:    binary.LittleEndian.Uint32(record[0:]),
54                 length: binary.LittleEndian.Uint32(record[4:]),
55                 offset: binary.LittleEndian.Uint64(record[8:]),
56         }
57 }
58
59 // Records is a file of fixed-size Records, numbered from 0. Record n starts at
60 // byte n*size. A record that was never written reads as zeros. An index is data
61 // that the day files can give again, so Records cuts a partial record at the
62 // end of the file.
63 type Records struct {
64         file  *os.File
65         size  int
66         count int
67 }
68
69 // Read returns record n, or false when the file does not reach it.
70 func (r *Records) Read(n int) ([]byte, bool, error) {
71         if n >= r.count {
72                 return nil, false, nil
73         }
74
75         record := make([]byte, r.size)
76         _, err := r.file.ReadAt(record, int64(n*r.size))
77         if err != nil && !errors.Is(err, io.EOF) {
78                 return nil, false, fmt.Errorf("read index record %d: %w", n, err)
79         }
80
81         return record, true, nil
82 }
83
84 // Write writes record n.
85 func (r *Records) Write(n int, record []byte) error {
86         _, err := r.file.WriteAt(record, int64(n*r.size))
87         if err != nil {
88                 return fmt.Errorf("write index record %d: %w", n, err)
89         }
90
91         r.count = max(r.count, n+1)
92
93         return nil
94 }
95
96 // Append writes the record after the last one.
97 func (r *Records) Append(record []byte) error {
98         return r.Write(r.count, record)
99 }
100
101 // Close closes the file.
102 func (r *Records) Close() error {
103         return r.file.Close()
104 }