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  }