carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package moves
2
3import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10
11 "github.com/superfly/ltx"
12)
13
14// sqliteHeaderSize is the SQLite database header, the first 100 bytes of
15// page 1.
16const sqliteHeaderSize = 100
17
18var sqliteMagic = []byte("SQLite format 3\x00")
19
20// ReadDBHeader reads the page size (header bytes 16-17; the value 1 means
21// 65536) and the in-header database size in pages (bytes 28-31) of the
22// SQLite database at path. The in-header size is what SQLite keeps current
23// in WAL mode and what litestream's restore writes; BuildGeneration checks
24// it against the file size before trusting it.
25func ReadDBHeader(path string) (pageSize uint32, pageCount uint32, err error) {
26 f, err := os.Open(path)
27 if err != nil {
28 return 0, 0, err
29 }
30 defer f.Close()
31 var hdr [sqliteHeaderSize]byte
32 if _, err := io.ReadFull(f, hdr[:]); err != nil {
33 return 0, 0, fmt.Errorf("read sqlite header of %s: %w", path, err)
34 }
35 if !bytes.Equal(hdr[:len(sqliteMagic)], sqliteMagic) {
36 return 0, 0, fmt.Errorf("%s is not a SQLite database", path)
37 }
38 pageSize = uint32(binary.BigEndian.Uint16(hdr[16:18]))
39 if pageSize == 1 {
40 pageSize = 65536
41 }
42 if !ltx.IsValidPageSize(pageSize) {
43 return 0, 0, fmt.Errorf("%s has an invalid page size %d", path, pageSize)
44 }
45 pageCount = binary.BigEndian.Uint32(hdr[28:32])
46 if pageCount == 0 {
47 return 0, 0, fmt.Errorf("%s has no in-header database size", path)
48 }
49 return pageSize, pageCount, nil
50}
51
52// EncodeFull encodes every page of db, pages 1 to pageCount in order, as
53// one LTX file [min, max] with the NoChecksum flag, and returns the bytes
54// and the trailer's CRC-64 file checksum. SQLite's lock page
55// (ltx.LockPgno(pageSize), the page holding PENDING_BYTE) is skipped: the
56// encoder refuses it, litestream never ships it, and the decoder writes it
57// back as zeros; Commit keeps the full page count. With min 1 the file is
58// a snapshot; with min == max == N and walOffset litestream.WALHeaderSize
59// it is the anchor litestream's open reads its position from (§5.1 step
60// 7). The header timestamp is an input, so the same inputs give the same
61// bytes, which is what lets a replayed PUT repeat its object. The bytes
62// are read back and verified before they are returned.
63func EncodeFull(db io.ReaderAt, pageSize, pageCount uint32, min, max ltx.TXID, timestamp int64, walOffset int64) (data []byte, crc64 uint64, err error) {
64 if pageCount == 0 {
65 return nil, 0, errors.New("moves: cannot encode an empty database")
66 }
67 var buf bytes.Buffer
68 enc, err := ltx.NewEncoder(&buf)
69 if err != nil {
70 return nil, 0, err
71 }
72 if err := enc.EncodeHeader(ltx.Header{
73 Version: ltx.Version,
74 Flags: ltx.HeaderFlagNoChecksum,
75 PageSize: pageSize,
76 Commit: pageCount,
77 MinTXID: min,
78 MaxTXID: max,
79 Timestamp: timestamp,
80 WALOffset: walOffset,
81 }); err != nil {
82 return nil, 0, fmt.Errorf("moves: encode header: %w", err)
83 }
84 lock := ltx.LockPgno(pageSize)
85 page := make([]byte, pageSize)
86 for pgno := uint32(1); pgno <= pageCount; pgno++ {
87 if pgno == lock {
88 continue
89 }
90 if _, err := db.ReadAt(page, int64(pgno-1)*int64(pageSize)); err != nil {
91 return nil, 0, fmt.Errorf("moves: read page %d: %w", pgno, err)
92 }
93 if err := enc.EncodePage(ltx.PageHeader{Pgno: pgno}, page); err != nil {
94 return nil, 0, fmt.Errorf("moves: encode page %d: %w", pgno, err)
95 }
96 }
97 if err := enc.Close(); err != nil {
98 return nil, 0, fmt.Errorf("moves: close encoder: %w", err)
99 }
100 data = buf.Bytes()
101 dec := ltx.NewDecoder(bytes.NewReader(data))
102 if err := dec.Verify(); err != nil {
103 return nil, 0, fmt.Errorf("moves: verify encoded %s: %w", ltx.FormatFilename(min, max), err)
104 }
105 if dec.Trailer().FileChecksum != enc.Trailer().FileChecksum {
106 return nil, 0, fmt.Errorf("moves: encoded %s reads back with another checksum", ltx.FormatFilename(min, max))
107 }
108 return data, uint64(dec.Trailer().FileChecksum), nil
109}
110