github.com/jfrog/jfrog-client-go@v1.40.2/utils/io/content/contentwriter.go (about) 1 package content 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "os" 9 "sync" 10 11 "github.com/jfrog/jfrog-client-go/utils" 12 "github.com/jfrog/jfrog-client-go/utils/errorutils" 13 "github.com/jfrog/jfrog-client-go/utils/log" 14 15 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 16 ) 17 18 const ( 19 jsonArrayPrefixPattern = " \"%s\": [" 20 jsonArraySuffix = "]\n" 21 DefaultKey = "results" 22 ) 23 24 // Write a JSON file in small chunks. Only a single JSON key can be written to the file, and array as its value. 25 // The array's values could be any JSON value types (number, string, etc...). 26 // Once the first 'Write" call is made, the file will stay open, waiting for the next struct to be written (thread-safe). 27 // Finally, 'Close' will fill the end of the JSON file and the operation will be completed. 28 type ContentWriter struct { 29 // arrayKey = JSON object key to be written. 30 arrayKey string 31 // The output data file path. 32 outputFile *os.File 33 // The chanel which from the output records will be pulled. 34 dataChannel chan interface{} 35 isCompleteFile bool 36 errorsQueue *utils.ErrorsQueue 37 runWaiter sync.WaitGroup 38 once sync.Once 39 empty bool 40 useStdout bool 41 } 42 43 func NewContentWriter(arrayKey string, isCompleteFile, useStdout bool) (*ContentWriter, error) { 44 self := ContentWriter{} 45 self.useStdout = useStdout 46 self.arrayKey = arrayKey 47 self.dataChannel = make(chan interface{}, utils.MaxBufferSize) 48 self.errorsQueue = utils.NewErrorsQueue(utils.MaxBufferSize) 49 self.isCompleteFile = isCompleteFile 50 self.empty = true 51 return &self, nil 52 } 53 54 func (rw *ContentWriter) SetArrayKey(arrKey string) *ContentWriter { 55 rw.arrayKey = arrKey 56 return rw 57 } 58 59 func (rw *ContentWriter) GetArrayKey() string { 60 return rw.arrayKey 61 } 62 63 func (rw *ContentWriter) IsEmpty() bool { 64 return rw.empty 65 } 66 67 func (rw *ContentWriter) GetFilePath() string { 68 if rw.outputFile != nil { 69 return rw.outputFile.Name() 70 } 71 return "" 72 } 73 74 func (rw *ContentWriter) RemoveOutputFilePath() error { 75 return errorutils.CheckError(os.Remove(rw.outputFile.Name())) 76 } 77 78 // Write a single item to the JSON array. 79 func (rw *ContentWriter) Write(record interface{}) { 80 rw.empty = false 81 rw.startWritingWorker() 82 rw.dataChannel <- record 83 } 84 85 func (rw *ContentWriter) startWritingWorker() { 86 rw.once.Do(func() { 87 var err error 88 if rw.useStdout { 89 rw.outputFile = os.Stdout 90 } else { 91 rw.outputFile, err = fileutils.CreateTempFile() 92 if err != nil { 93 rw.errorsQueue.AddError(errorutils.CheckError(err)) 94 return 95 } 96 } 97 rw.runWaiter.Add(1) 98 go func() { 99 defer rw.runWaiter.Done() 100 rw.run() 101 }() 102 }) 103 } 104 105 // Write the data from the channel to JSON file. 106 // The channel may block the thread, therefore should run async. 107 func (rw *ContentWriter) run() { 108 var err error 109 if !rw.useStdout { 110 defer func() { 111 if err = errors.Join(rw.outputFile.Sync(), rw.outputFile.Close()); err != nil { 112 rw.errorsQueue.AddError(errorutils.CheckError(err)) 113 } 114 }() 115 } 116 openString := jsonArrayPrefixPattern 117 closeString := "" 118 if rw.isCompleteFile { 119 openString = "{\n" + openString 120 } 121 _, err = rw.outputFile.WriteString(fmt.Sprintf(openString, rw.arrayKey)) 122 if err != nil { 123 rw.errorsQueue.AddError(errorutils.CheckError(err)) 124 return 125 } 126 buf := bytes.NewBuffer(nil) 127 enc := json.NewEncoder(buf) 128 enc.SetIndent(" ", " ") 129 recordPrefix := "\n " 130 firstRecord := true 131 for record := range rw.dataChannel { 132 buf.Reset() 133 err = enc.Encode(record) 134 if err != nil { 135 rw.errorsQueue.AddError(errorutils.CheckError(err)) 136 continue 137 } 138 record := recordPrefix + string(bytes.TrimRight(buf.Bytes(), "\n")) 139 _, err = rw.outputFile.WriteString(record) 140 if err != nil { 141 rw.errorsQueue.AddError(errorutils.CheckError(err)) 142 continue 143 } 144 if firstRecord { 145 // If a record was printed, we want to print a comma and ne before each and every future record. 146 recordPrefix = "," + recordPrefix 147 // We will close the array in a new-indent line. 148 closeString = "\n " 149 firstRecord = false 150 } 151 } 152 closeString += jsonArraySuffix 153 if rw.isCompleteFile { 154 closeString += "}\n" 155 } 156 _, err = rw.outputFile.WriteString(closeString) 157 if err != nil { 158 rw.errorsQueue.AddError(errorutils.CheckError(err)) 159 } 160 } 161 162 // Finish writing to the file. 163 func (rw *ContentWriter) Close() error { 164 if rw.empty { 165 return nil 166 } 167 close(rw.dataChannel) 168 rw.runWaiter.Wait() 169 if err := rw.GetError(); err != nil { 170 log.Error("Failed to close writer: " + err.Error()) 171 return err 172 } 173 return nil 174 } 175 176 func (rw *ContentWriter) GetError() error { 177 return rw.errorsQueue.GetError() 178 }