1package store
2
3import (
4 "encoding/binary"
5 "fmt"
6 "os"
7 "path/filepath"
8 "sync"
9)
10
11const (
12 packMagic = "ARCHE-PACK-V1\x00\x00"
13 defaultPackSealSize = 256 * 1024 * 1024
14)
15
16type packManager struct {
17 dir string
18 mu sync.Mutex
19 cur *os.File
20 size int64
21 name string
22 sealSize int64
23}
24
25func newPackManager(dir string, sealSize int) (*packManager, error) {
26 if err := os.MkdirAll(dir, 0o755); err != nil {
27 return nil, fmt.Errorf("pack: create dir %s: %w", dir, err)
28 }
29 ss := int64(sealSize)
30 if ss <= 0 {
31 ss = defaultPackSealSize
32 }
33 pm := &packManager{dir: dir, sealSize: ss}
34 return pm, nil
35}
36
37func (pm *packManager) close() {
38 pm.mu.Lock()
39 defer pm.mu.Unlock()
40 if pm.cur != nil {
41 _ = pm.cur.Close()
42 pm.cur = nil
43 }
44}
45
46type packEntry struct {
47 packFile string
48 offset int64
49 rawSize int64
50}
51
52func (pm *packManager) write(compressed []byte, rawSize int64) (packEntry, error) {
53 pm.mu.Lock()
54 defer pm.mu.Unlock()
55
56 if pm.cur == nil || pm.size >= pm.sealSize {
57 if err := pm.openNewPack(); err != nil {
58 return packEntry{}, err
59 }
60 }
61
62 offset := pm.size
63
64 hdr := make([]byte, 16)
65 binary.BigEndian.PutUint64(hdr[0:8], uint64(rawSize))
66 binary.BigEndian.PutUint64(hdr[8:16], uint64(len(compressed)))
67 _, err := pm.cur.Write(hdr)
68 if err != nil {
69 return packEntry{}, fmt.Errorf("pack write header: %w", err)
70 }
71 _, err = pm.cur.Write(compressed)
72 if err != nil {
73 return packEntry{}, fmt.Errorf("pack write data: %w", err)
74 }
75
76 pm.size += int64(16 + len(compressed))
77 return packEntry{packFile: pm.name, offset: offset, rawSize: rawSize}, nil
78}
79
80func (pm *packManager) read(packFile string, offset int64) ([]byte, error) {
81 path := filepath.Join(pm.dir, packFile)
82 f, err := os.Open(path)
83 if err != nil {
84 return nil, fmt.Errorf("pack open %s: %w", packFile, err)
85 }
86 defer f.Close()
87
88 magic := make([]byte, len(packMagic))
89 if _, err := readFull(f, magic); err != nil {
90 return nil, fmt.Errorf("pack %s: cannot read magic header: %w", packFile, err)
91 }
92 if string(magic) != packMagic {
93 return nil, fmt.Errorf("pack %s: invalid magic header (file may be corrupt or not an Arche pack)", packFile)
94 }
95
96 if _, err := f.Seek(offset, 0); err != nil {
97 return nil, fmt.Errorf("pack seek: %w", err)
98 }
99
100 hdr := make([]byte, 16)
101 if _, err := readFull(f, hdr); err != nil {
102 return nil, fmt.Errorf("pack read header: %w", err)
103 }
104 compSize := binary.BigEndian.Uint64(hdr[8:16])
105
106 comp := make([]byte, compSize)
107 if _, err := readFull(f, comp); err != nil {
108 return nil, fmt.Errorf("pack read data: %w", err)
109 }
110 return comp, nil
111}
112
113func (pm *packManager) openNewPack() error {
114 if pm.cur != nil {
115 if err := pm.cur.Sync(); err != nil {
116 return fmt.Errorf("pack sync: %w", err)
117 }
118 if err := pm.cur.Close(); err != nil {
119 return fmt.Errorf("pack close: %w", err)
120 }
121 }
122
123 name := fmt.Sprintf("%016x.pack", uniquePackID())
124 path := filepath.Join(pm.dir, name)
125 f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o644)
126 if err != nil {
127 return fmt.Errorf("pack create %s: %w", name, err)
128 }
129
130 if _, err := f.WriteString(packMagic); err != nil {
131 f.Close()
132 return fmt.Errorf("pack write magic: %w", err)
133 }
134
135 pm.cur = f
136 pm.name = name
137 pm.size = int64(len(packMagic))
138 return nil
139}
140
141var (
142 packCounter uint64
143 packMu sync.Mutex
144)
145
146func uniquePackID() uint64 {
147 packMu.Lock()
148 defer packMu.Unlock()
149 packCounter++
150 return uint64(os.Getpid())<<32 | packCounter
151}
152
153func readFull(f *os.File, buf []byte) (int, error) {
154 total := 0
155 for total < len(buf) {
156 n, err := f.Read(buf[total:])
157 total += n
158 if err != nil {
159 return total, err
160 }
161 }
162 return total, nil
163}