返回 DeepSeek-Reasonix
service.go
根目录 / internal / attachment / service.go
1 package attachment
2
3 import (
4 "bytes"
5 "context"
6 "crypto/rand"
7 "encoding/hex"
8 "io"
9 "maps"
10 "sync"
11
12 "reasonix/internal/sessioncontent"
13 )
14
15 // Service validates images and publishes originals into a session content store.
16 type Service struct {
17 store *sessioncontent.Store
18 policy Policy
19 cache *VariantCache
20 drafts *DraftStore
21 }
22
23 func NewService(store *sessioncontent.Store, cache *VariantCache) *Service {
24 if cache == nil {
25 cache = NewVariantCache(DefaultCacheBytes, DefaultTransforms)
26 }
27 return &Service{store: store, policy: DefaultPolicy(), cache: cache, drafts: NewDraftStore()}
28 }
29
30 func (s *Service) WithPolicy(policy Policy) *Service {
31 if s == nil {
32 return nil
33 }
34 out := *s
35 out.policy = policy.withDefaults()
36 return &out
37 }
38
39 func (s *Service) Store() *sessioncontent.Store {
40 if s == nil {
41 return nil
42 }
43 return s.store
44 }
45
46 func (s *Service) Drafts() *DraftStore {
47 if s == nil {
48 return nil
49 }
50 return s.drafts
51 }
52
53 func (s *Service) Cache() *VariantCache {
54 if s == nil {
55 return nil
56 }
57 return s.cache
58 }
59
60 func (s *Service) PrepareBatch(ctx context.Context, sources []Source) (PreparedImages, error) {
61 if s == nil {
62 return PreparedImages{}, Error{Code: CodeUnreadable, Message: "attachment service unavailable"}
63 }
64 if err := ctx.Err(); err != nil {
65 return PreparedImages{}, canceledError(err)
66 }
67 policy := s.policy.withDefaults()
68 if len(sources) > policy.MaxCount {
69 return PreparedImages{}, Error{Code: CodeTooMany, Message: defaultDetail(CodeTooMany)}
70 }
71 out := PreparedImages{Items: make([]PreparedImage, 0, len(sources))}
72 var failures BatchError
73 var total int64
74 for i, source := range sources {
75 if err := ctx.Err(); err != nil {
76 return PreparedImages{}, canceledError(err)
77 }
78 item, err := s.prepareSource(ctx, source, policy)
79 if err != nil {
80 itemErr := asError(err)
81 itemErr.Index = i + 1
82 if itemErr.Name == "" {
83 itemErr.Name = source.displayName()
84 }
85 failures = append(failures, itemErr)
86 continue
87 }
88 size := int64(len(item.Bytes))
89 if item.Existing != nil {
90 size = item.Existing.Content.Bytes
91 }
92 total += size
93 if total > policy.MaxBatchBytes {
94 failures = append(failures, Error{Code: CodeBatchSize, Name: item.DisplayName, Index: i + 1, Message: defaultDetail(CodeBatchSize)})
95 continue
96 }
97 out.Items = append(out.Items, item)
98 }
99 if len(failures) > 0 {
100 return PreparedImages{}, failures
101 }
102 return out, nil
103 }
104
105 func (s *Service) prepareSource(ctx context.Context, source Source, policy Policy) (PreparedImage, error) {
106 select {
107 case s.cache.transforms <- struct{}{}:
108 defer func() { <-s.cache.transforms }()
109 case <-ctx.Done():
110 return PreparedImage{}, canceledError(ctx.Err())
111 }
112 if err := ctx.Err(); err != nil {
113 return PreparedImage{}, canceledError(err)
114 }
115 if source.Existing == nil {
116 return source.Read(ctx, policy)
117 }
118 ref := source.Existing
119 if ref.Content.Bytes > policy.MaxBytes {
120 return PreparedImage{}, Error{Code: CodeSize, Message: defaultDetail(CodeSize)}
121 }
122 raw, err := s.ReadVerified(ctx, *ref)
123 if err != nil {
124 return PreparedImage{}, err
125 }
126 item, err := (Source{Bytes: raw, DeclaredMIME: ref.MIME(), DisplayName: source.displayName()}).Read(ctx, policy)
127 if err != nil {
128 return PreparedImage{}, err
129 }
130 if item.Width != ref.Width || item.Height != ref.Height {
131 return PreparedImage{}, Error{Code: CodeCorrupt, Message: defaultDetail(CodeCorrupt)}
132 }
133 item.Existing = ref
134 item.Bytes = nil
135 return item, nil
136 }
137
138 func (s *Service) CommitBatch(ctx context.Context, prepared PreparedImages) ([]AttachmentRef, error) {
139 if s == nil || s.store == nil {
140 return nil, Error{Code: CodeUnreadable, Message: "attachment store unavailable"}
141 }
142 if err := ctx.Err(); err != nil {
143 return nil, canceledError(err)
144 }
145 refs := make([]AttachmentRef, 0, len(prepared.Items))
146 for i, item := range prepared.Items {
147 if err := ctx.Err(); err != nil {
148 return nil, canceledError(err)
149 }
150 if item.Existing != nil {
151 if err := s.store.Verify(ctx, item.Existing.Content); err != nil {
152 return nil, Error{Code: CodeCorrupt, Name: item.DisplayName, Index: i + 1, Message: defaultDetail(CodeCorrupt), Cause: err}
153 }
154 refs = append(refs, *item.Existing)
155 continue
156 }
157 ref, err := s.store.Put(ctx, bytes.NewReader(item.Bytes), sessioncontent.Metadata{MediaType: item.MIME, Name: item.DisplayName})
158 if err != nil {
159 return nil, Error{Code: CodeUnreadable, Name: item.DisplayName, Index: i + 1, Message: "could not persist the image", Cause: err, Retry: true}
160 }
161 refs = append(refs, AttachmentRef{
162 Version: RefVersion,
163 Content: ref,
164 Width: item.Width,
165 Height: item.Height,
166 DisplayName: item.DisplayName,
167 })
168 }
169 return refs, nil
170 }
171
172 func (s *Service) ReadVerified(ctx context.Context, ref AttachmentRef) ([]byte, error) {
173 if s == nil || s.store == nil {
174 return nil, Error{Code: CodeUnreadable, Message: "attachment store unavailable"}
175 }
176 if err := ref.Validate(); err != nil {
177 return nil, err
178 }
179 f, err := s.store.Open(ctx, ref.Content)
180 if err != nil {
181 return nil, Error{Code: CodeUnreadable, Name: ref.DisplayName, Message: defaultDetail(CodeUnreadable), Cause: err}
182 }
183 defer f.Close()
184 raw, err := io.ReadAll(f)
185 if err != nil {
186 return nil, Error{Code: CodeUnreadable, Name: ref.DisplayName, Message: defaultDetail(CodeUnreadable), Cause: err}
187 }
188 if int64(len(raw)) != ref.Content.Bytes {
189 return nil, Error{Code: CodeCorrupt, Name: ref.DisplayName, Message: defaultDetail(CodeCorrupt)}
190 }
191 return raw, nil
192 }
193
194 func (s *Service) InputsFromRefs(refs []AttachmentRef) []ImageInput {
195 out := make([]ImageInput, 0, len(refs))
196 for i := range refs {
197 ref := refs[i]
198 out = append(out, ImageInput{Kind: KindAttachment, Attachment: &ref})
199 }
200 return out
201 }
202
203 func asError(err error) Error {
204 var item Error
205 if err != nil && errorsAs(err, &item) {
206 return item
207 }
208 return Error{Code: CodeUnreadable, Message: defaultDetail(CodeUnreadable), Cause: err, Retry: true}
209 }
210
211 func newID() string {
212 var b [16]byte
213 if _, err := rand.Read(b[:]); err != nil {
214 panic("attachment: random id: " + err.Error())
215 }
216 return hex.EncodeToString(b[:])
217 }
218
219 // DraftStore issues opaque draft credentials bound to a caller-chosen key.
220 type DraftStore struct {
221 mu sync.Mutex
222 items map[string]draftRecord
223 }
224
225 type draftRecord struct {
226 scope string
227 draft DraftCredential
228 family string
229 needsRebind bool
230 }
231
232 func NewDraftStore() *DraftStore {
233 return &DraftStore{items: map[string]draftRecord{}}
234 }
235
236 func (d *DraftStore) Issue(scope string, ref AttachmentRef) DraftCredential {
237 if d == nil {
238 d = NewDraftStore()
239 }
240 draft := DraftCredential{
241 ID: newID(),
242 Ref: ref,
243 DisplayName: ref.DisplayName,
244 MIME: ref.MIME(),
245 Width: ref.Width,
246 Height: ref.Height,
247 Bytes: ref.Content.Bytes,
248 }
249 d.mu.Lock()
250 d.items[draft.ID] = draftRecord{scope: scope, draft: draft, family: draft.ID}
251 d.mu.Unlock()
252 return draft
253 }
254
255 func (d *DraftStore) Lookup(scope, id string) (DraftCredential, bool) {
256 if d == nil {
257 return DraftCredential{}, false
258 }
259 d.mu.Lock()
260 defer d.mu.Unlock()
261 item, ok := d.items[id]
262 if !ok || item.scope != scope {
263 return DraftCredential{}, false
264 }
265 return item.draft, true
266 }
267
268 func (d *DraftStore) Resolve(scope string, ids []string) ([]AttachmentRef, error) {
269 refs := make([]AttachmentRef, 0, len(ids))
270 for i, id := range ids {
271 draft, ok := d.Lookup(scope, id)
272 if !ok {
273 return nil, Error{Code: CodeMissing, Name: "image", Index: i + 1, Message: "draft credential is not valid", Retry: true}
274 }
275 refs = append(refs, draft.Ref)
276 }
277 return refs, nil
278 }
279
280 func (d *DraftStore) Release(scope, id string) {
281 if d == nil {
282 return
283 }
284 d.mu.Lock()
285 defer d.mu.Unlock()
286 item, ok := d.items[id]
287 if ok && item.scope == scope {
288 for key, related := range d.items {
289 if related.scope == scope && related.family == item.family {
290 delete(d.items, key)
291 }
292 }
293 }
294 }
295
296 func (d *DraftStore) ReleaseScope(scope string) {
297 if d == nil {
298 return
299 }
300 d.mu.Lock()
301 defer d.mu.Unlock()
302 for id, item := range d.items {
303 if item.scope == scope {
304 delete(d.items, id)
305 }
306 }
307 }
308
309 // CopyScopeTo transfers credentials during an owner-authorized replacement.
310 // It cannot grant access to another session or storage generation.
311 func (d *DraftStore) CopyScopeTo(scope string, target *DraftStore) {
312 if d == nil || target == nil || d == target {
313 return
314 }
315 d.mu.Lock()
316 items := make(map[string]draftRecord)
317 for id, item := range d.items {
318 if item.scope == scope {
319 item.needsRebind = true
320 items[id] = item
321 }
322 }
323 d.mu.Unlock()
324 target.mu.Lock()
325 defer target.mu.Unlock()
326 maps.Copy(target.items, items)
327 }
328
328 lines GO