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