返回 DeepSeek-Reasonix
publish.go
根目录 / internal / tool / builtin / publish.go
1 package builtin
2
3 import (
4 "context"
5 "crypto/sha256"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10
11 "reasonix/internal/fileops"
12 "reasonix/internal/tool"
13 )
14
15 // ErrFileChanged is returned when a structured write tool re-reads the target
16 // and the native identity, freshness metadata, or SHA-256 no longer match.
17 var ErrFileChanged = errors.New("file changed after it was read")
18
19 type fileIdentity struct {
20 existed bool
21 overlay bool
22 sum [sha256.Size]byte
23 mode os.FileMode
24 target fileops.Target
25 version fileops.Version
26 }
27
28 func diskIdentity(path string) (fileIdentity, error) {
29 _, id, err := readDiskIdentity(path)
30 return id, err
31 }
32
33 // Bytes, identity and freshness metadata must describe the same open source.
34 func readDiskIdentity(path string) ([]byte, fileIdentity, error) {
35 f, err := os.Open(path)
36 if err != nil {
37 if os.IsNotExist(err) {
38 return nil, fileIdentity{}, nil
39 }
40 return nil, fileIdentity{}, err
41 }
42 defer f.Close()
43 fi, err := f.Stat()
44 if err != nil {
45 return nil, fileIdentity{}, err
46 }
47 if !fi.Mode().IsRegular() {
48 return nil, fileIdentity{}, fmt.Errorf("%s is not a regular file", path)
49 }
50 target, version := fileops.DiskHandleSnapshot(path, f, fi)
51 b, err := io.ReadAll(f)
52 if err != nil {
53 return nil, fileIdentity{}, err
54 }
55 after, err := f.Stat()
56 if err != nil {
57 return nil, fileIdentity{}, err
58 }
59 endTarget, endVersion := fileops.DiskHandleSnapshot(path, f, after)
60 current, err := os.Stat(path)
61 if err != nil {
62 return nil, fileIdentity{}, err
63 }
64 pathTarget, pathVersion := fileops.DiskSnapshot(path, current)
65 if target != endTarget || target != pathTarget || version != endVersion || version != pathVersion {
66 return nil, fileIdentity{}, fmt.Errorf("%w: %s", ErrFileChanged, path)
67 }
68 return b, fileIdentity{existed: true, sum: sha256.Sum256(b), mode: fi.Mode().Perm(), target: target, version: version}, nil
69 }
70
71 func overlayIdentity(content string) fileIdentity {
72 return fileIdentity{existed: true, overlay: true, sum: sha256.Sum256([]byte(content))}
73 }
74
75 func (id fileIdentity) equal(other fileIdentity) bool {
76 return id.existed == other.existed && id.overlay == other.overlay &&
77 id.sum == other.sum && id.mode == other.mode && id.target == other.target && id.version == other.version
78 }
79
80 func (s editSource) assertUnchanged(ctx context.Context, overlay FileOverlay, path string) error {
81 now, err := s.currentIdentity(ctx, overlay, path)
82 if err != nil {
83 return err
84 }
85 if !s.id.equal(now) {
86 return &tool.OperationError{Diagnostic: tool.OperationDiagnostic{Code: tool.FSStaleVersion, Path: path, ExpectedSnapshot: s.readSnapshot(path), Recovery: "read the current file again, then retry"}, Cause: fmt.Errorf("%w: %s", ErrFileChanged, path)}
87 }
88 return nil
89 }
90
91 func (s editSource) currentIdentity(ctx context.Context, overlay FileOverlay, path string) (fileIdentity, error) {
92 if s.overlay && overlay != nil {
93 if buffered, ok := overlay.ReadTextFile(ctx, path); ok {
94 return overlayIdentity(buffered), nil
95 }
96 }
97 return diskIdentity(path)
98 }
99
99 lines GO