@@ -2,6 +2,7 @@ package artifact
22
33import (
44 "encoding/json"
5+ "io"
56 "os"
67 "path/filepath"
78
@@ -88,6 +89,48 @@ func LoadConfigFromFile(path string) (*Config, error) {
8889 treePath := filepath .Join (pathDir , TreeName )
8990 config .configDir = pathDir
9091 config .commitFn = func () error {
92+ // Read current version from disk to detect concurrent modifications
93+ var currentVersion int64
94+ var fileExists bool
95+ //nolint:gosec
96+ if existingFile , err := os .Open (treePath ); err == nil {
97+ defer utils .UncheckedErrorFunc (existingFile .Close )
98+
99+ fileExists = true
100+ var existingTreeFile TreeFile
101+ if err := json .NewDecoder (existingFile ).Decode (& existingTreeFile ); err != nil {
102+ // If we get EOF or decode error, the file might be being written by another process
103+ // Treat this as a concurrent modification
104+ if err .Error () == "EOF" || errors .Is (err , os .ErrClosed ) {
105+ return NewConflictError (treePath , config .treeVersion , - 1 )
106+ }
107+
108+ // Try backward compatible read
109+ if _ , seekErr := existingFile .Seek (0 , 0 ); seekErr == nil {
110+ var tree TreeNodeTree
111+ if decodeErr := json .NewDecoder (existingFile ).Decode (& tree ); decodeErr == nil {
112+ currentVersion = 0 // Old format
113+ } else {
114+ return errors .Wrap (err , "failed to read existing tree version" )
115+ }
116+ } else {
117+ return errors .Wrap (err , "failed to read existing tree version" )
118+ }
119+ } else {
120+ currentVersion = existingTreeFile .Version
121+ }
122+ }
123+
124+ // Check for concurrent modification (optimistic locking)
125+ // Only check if file existed when we tried to read it
126+ if fileExists && currentVersion != config .treeVersion {
127+ return NewConflictError (treePath , config .treeVersion , currentVersion )
128+ }
129+
130+ // Increment version for this write
131+ newVersion := config .treeVersion + 1
132+
133+ // Write new tree with incremented version
91134 //nolint:gosec
92135 newTreeFile , err := os .OpenFile (treePath , os .O_RDWR | os .O_CREATE , 0o600 )
93136 if err != nil {
@@ -97,25 +140,56 @@ func LoadConfigFromFile(path string) (*Config, error) {
97140 if err := newTreeFile .Truncate (0 ); err != nil {
98141 return err
99142 }
143+
144+ treeFile := TreeFile {
145+ Version : newVersion ,
146+ Tree : config .tree ,
147+ }
148+
100149 enc := json .NewEncoder (newTreeFile )
101150 enc .SetIndent ("" , " " )
102- return enc .Encode (config .tree )
151+ if err := enc .Encode (treeFile ); err != nil {
152+ return err
153+ }
154+
155+ // Update in-memory version on successful write
156+ config .treeVersion = newVersion
157+ return nil
103158 }
104159
105160 //nolint:gosec
106- treeFile , err := os .Open (treePath )
161+ treeFileHandle , err := os .Open (treePath )
107162 if err == nil {
108- defer utils .UncheckedErrorFunc (treeFile .Close )
163+ defer utils .UncheckedErrorFunc (treeFileHandle .Close )
109164
110- treeDec := json .NewDecoder (treeFile )
165+ // Read file contents for potential backward compatibility
166+ var fileData []byte
167+ fileData , err = io .ReadAll (treeFileHandle )
168+ if err != nil {
169+ if err .Error () == "EOF" {
170+ return nil , errors .New ("tree file is being modified by another process, please retry" )
171+ }
172+ return nil , errors .Wrap (err , "failed to read tree file" )
173+ }
111174
112- var tree TreeNodeTree
113- if err := treeDec .Decode (& tree ); err != nil {
114- return nil , err
175+ // Try to decode as new format (TreeFile with version)
176+ var treeFile TreeFile
177+ if err := json .Unmarshal (fileData , & treeFile ); err == nil && treeFile .Tree != nil && len (treeFile .Tree ) > 0 {
178+ // Successfully decoded as new format with content
179+ config .tree = treeFile .Tree
180+ config .treeVersion = treeFile .Version
181+ } else {
182+ // Try backward compatibility: decode as raw tree without version wrapper
183+ var tree TreeNodeTree
184+ if err := json .Unmarshal (fileData , & tree ); err != nil {
185+ return nil , errors .Wrap (err , "failed to decode tree file in old or new format" )
186+ }
187+ config .tree = tree
188+ config .treeVersion = 0 // Old format, start at version 0
115189 }
116- config .tree = tree
117190 } else {
118191 config .tree = TreeNodeTree {}
192+ config .treeVersion = 0
119193 }
120194
121195 return & config , nil
0 commit comments