返回 DeepSeek-Reasonix
blob.go
根目录 / internal / checkpoint / blob.go
1 package checkpoint
2
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "os"
8 "path/filepath"
9 "sync"
10
11 "reasonix/internal/fileutil"
12 )
13
14 // BlobStore is a content-addressed store for checkpoint file payloads.
15 // Blobs are named by SHA-256 hex of their contents and written atomically.
16 type BlobStore struct {
17 dir string
18 mu sync.Mutex
19 }
20
21 // NewBlobStore creates a blob store under dir (usually <session>.ckpt/blobs).
22 // An empty dir makes Put/Get operate as no-ops that return errors for Get.
23 func NewBlobStore(dir string) *BlobStore {
24 return &BlobStore{dir: dir}
25 }
26
27 // Dir returns the blob directory.
28 func (b *BlobStore) Dir() string {
29 if b == nil {
30 return ""
31 }
32 return b.dir
33 }
34
35 // Put stores data if not already present and returns the content digest.
36 func (b *BlobStore) Put(data []byte) (string, error) {
37 if b == nil || b.dir == "" {
38 return "", fmt.Errorf("blob store unavailable")
39 }
40 sum := sha256.Sum256(data)
41 ref := hex.EncodeToString(sum[:])
42 path := b.path(ref)
43
44 b.mu.Lock()
45 defer b.mu.Unlock()
46 if st, err := os.Stat(path); err == nil && st.Size() == int64(len(data)) {
47 if existing, readErr := os.ReadFile(path); readErr == nil && Digest(existing) == ref {
48 return ref, nil
49 }
50 }
51 if err := os.MkdirAll(b.dir, 0o755); err != nil {
52 return "", err
53 }
54 if err := fileutil.AtomicWriteFileStrict(path, data, 0o644); err != nil {
55 return "", fmt.Errorf("write blob %s: %w", ref, err)
56 }
57 return ref, nil
58 }
59
60 // Get returns the blob bytes for ref.
61 func (b *BlobStore) Get(ref string) ([]byte, error) {
62 if b == nil || b.dir == "" {
63 return nil, fmt.Errorf("blob store unavailable")
64 }
65 if !validBlobRef(ref) {
66 return nil, fmt.Errorf("invalid blob ref %q", ref)
67 }
68 data, err := os.ReadFile(b.path(ref))
69 if err != nil {
70 return nil, err
71 }
72 if got := Digest(data); got != ref {
73 return nil, fmt.Errorf("blob %s failed content-address verification: got %s", ref, got)
74 }
75 return data, nil
76 }
77
78 // Has reports whether ref exists.
79 func (b *BlobStore) Has(ref string) bool {
80 if b == nil || b.dir == "" || !validBlobRef(ref) {
81 return false
82 }
83 _, err := b.Get(ref)
84 return err == nil
85 }
86
87 // Remove deletes a blob. Missing refs are ignored.
88 func (b *BlobStore) Remove(ref string) error {
89 if b == nil || b.dir == "" || !validBlobRef(ref) {
90 return nil
91 }
92 err := os.Remove(b.path(ref))
93 if os.IsNotExist(err) {
94 return nil
95 }
96 return err
97 }
98
99 // Prune removes content-addressed blobs not present in live. It walks the
100 // fan-out tree and never treats directory names as blob references.
101 func (b *BlobStore) Prune(live map[string]struct{}) error {
102 if b == nil || b.dir == "" {
103 return nil
104 }
105 b.mu.Lock()
106 defer b.mu.Unlock()
107 return filepath.WalkDir(b.dir, func(path string, entry os.DirEntry, err error) error {
108 if err != nil {
109 if os.IsNotExist(err) {
110 return nil
111 }
112 return err
113 }
114 if entry.IsDir() {
115 return nil
116 }
117 ref := entry.Name()
118 if !validBlobRef(ref) {
119 return nil
120 }
121 if _, ok := live[ref]; ok {
122 return nil
123 }
124 if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
125 return err
126 }
127 return nil
128 })
129 }
130
131 // Size returns total bytes of all blobs.
132 func (b *BlobStore) Size() (int64, error) {
133 if b == nil || b.dir == "" {
134 return 0, nil
135 }
136 var total int64
137 err := filepath.WalkDir(b.dir, func(_ string, entry os.DirEntry, err error) error {
138 if err != nil {
139 if os.IsNotExist(err) {
140 return nil
141 }
142 return err
143 }
144 if entry.IsDir() {
145 return nil
146 }
147 info, err := entry.Info()
148 if err != nil {
149 return err
150 }
151 total += info.Size()
152 return nil
153 })
154 if os.IsNotExist(err) {
155 return 0, nil
156 }
157 return total, err
158 }
159
160 func (b *BlobStore) path(ref string) string {
161 // Two-level fan-out to keep directory listings reasonable.
162 if len(ref) < 4 {
163 return filepath.Join(b.dir, ref)
164 }
165 return filepath.Join(b.dir, ref[:2], ref[2:4], ref)
166 }
167
168 func validBlobRef(ref string) bool {
169 if len(ref) != 64 {
170 return false
171 }
172 for _, c := range ref {
173 if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
174 return false
175 }
176 }
177 return true
178 }
179
180 // Digest returns the SHA-256 hex digest of data.
181 func Digest(data []byte) string {
182 sum := sha256.Sum256(data)
183 return hex.EncodeToString(sum[:])
184 }
185
185 lines GO