arche / internal/store/pack.go

commit 154431fd
  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}