github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/transferfiles/longpropertycheck.go (about) 1 package transferfiles 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io" 8 "sync" 9 10 "github.com/jfrog/gofrog/parallel" 11 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/api" 12 cmdutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils" 13 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils/precheckrunner" 14 "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" 15 "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" 16 "github.com/jfrog/jfrog-cli-core/v2/utils/progressbar" 17 "github.com/jfrog/jfrog-client-go/artifactory" 18 servicesUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" 19 clientutils "github.com/jfrog/jfrog-client-go/utils" 20 "golang.org/x/exp/slices" 21 22 "github.com/jfrog/jfrog-client-go/utils/log" 23 24 "time" 25 26 "github.com/jfrog/jfrog-client-go/utils/errorutils" 27 ) 28 29 const ( 30 propertyAqlPaginationLimit = 1000000 31 maxThreadCapacity = 5000000 32 threadCount = 10 33 maxAllowedValLength = 2400 34 longPropertyCheckName = "Properties with value longer than 2.4K characters" 35 propertiesRequestTimeout = time.Minute * 30 36 ) 37 38 // Property - Represents a property of an item 39 type Property struct { 40 Key string `json:"key,omitempty"` 41 Value string `json:"value,omitempty"` 42 } 43 44 // valueLength - Equals to the value length 45 func (p *Property) valueLength() uint { 46 return uint(len(p.Value)) 47 } 48 49 // FileWithLongProperty - Represent a failed instance; File with property that has failed the check (used for csv report format) 50 type FileWithLongProperty struct { 51 // The file that contains the long property 52 api.FileRepresentation 53 // The length of the property's value 54 Length uint `json:"value-length,omitempty"` 55 // The property that failed the check 56 Property 57 } 58 59 type LongPropertyCheck struct { 60 producerConsumer parallel.Runner 61 filesChan chan FileWithLongProperty 62 errorsQueue *clientutils.ErrorsQueue 63 repos []string 64 disabledDistinctiveAql bool 65 } 66 67 func NewLongPropertyCheck(repos []string, disabledDistinctiveAql bool) *LongPropertyCheck { 68 return &LongPropertyCheck{repos: repos, disabledDistinctiveAql: disabledDistinctiveAql} 69 } 70 71 func (lpc *LongPropertyCheck) Name() string { 72 return longPropertyCheckName 73 } 74 75 func (lpc *LongPropertyCheck) ExecuteCheck(args precheckrunner.RunArguments) (passed bool, err error) { 76 // Init producer consumer 77 lpc.producerConsumer = parallel.NewRunner(threadCount, maxThreadCapacity, false) 78 lpc.filesChan = make(chan FileWithLongProperty, threadCount) 79 lpc.errorsQueue = clientutils.NewErrorsQueue(1) 80 var waitCollection sync.WaitGroup 81 var filesWithLongProperty []FileWithLongProperty 82 // Handle progress display 83 var progress *progressbar.TasksProgressBar 84 if args.ProgressMng != nil { 85 progress = args.ProgressMng.NewTasksProgressBar(0, coreutils.IsWindows(), "long property") 86 defer progress.GetBar().Abort(true) 87 } 88 // Create consumer routine to collect the files from the search tasks 89 waitCollection.Add(1) 90 go func() { 91 for current := range lpc.filesChan { 92 filesWithLongProperty = append(filesWithLongProperty, current) 93 } 94 waitCollection.Done() 95 }() 96 // Create producer routine to create search tasks for long properties in the server 97 go func() { 98 defer lpc.producerConsumer.Done() 99 lpc.longPropertiesTaskProducer(progress, args) 100 }() 101 // Run 102 lpc.producerConsumer.Run() 103 close(lpc.filesChan) 104 waitCollection.Wait() 105 if err = lpc.errorsQueue.GetError(); err != nil { 106 return 107 } 108 // Result 109 if len(filesWithLongProperty) != 0 { 110 err = handleFailureRun(filesWithLongProperty) 111 } else { 112 passed = true 113 } 114 return 115 } 116 117 // Search for long properties in the server and create a search task to find the files that contains them 118 // Returns the number of long properties found 119 func (lpc *LongPropertyCheck) longPropertiesTaskProducer(progress *progressbar.TasksProgressBar, args precheckrunner.RunArguments) int { 120 // Init 121 serviceManager, err := utils.CreateServiceManagerWithContext(args.Context, args.ServerDetails, false, 0, retries, retriesWaitMilliSecs, propertiesRequestTimeout) 122 if err != nil { 123 return 0 124 } 125 var propertyQuery *AqlPropertySearchResult 126 longPropertiesCount := 0 127 pageCounter := 0 128 // Search 129 for { 130 if propertyQuery, err = runSearchPropertyAql(serviceManager, pageCounter); err != nil { 131 return 0 132 } 133 log.Debug(fmt.Sprintf("Found %d properties in the batch (isLastBatch=%t)", len(propertyQuery.Results), len(propertyQuery.Results) < propertyAqlPaginationLimit)) 134 for _, property := range propertyQuery.Results { 135 if long := isLongProperty(property); long { 136 log.Debug(fmt.Sprintf(`Found long property ('@%s':'%s')`, property.Key, property.Value)) 137 if lpc.producerConsumer != nil { 138 _, _ = lpc.producerConsumer.AddTaskWithError(lpc.createSearchPropertyTask(property, args, progress), lpc.errorsQueue.AddError) 139 } 140 if progress != nil { 141 progress.IncGeneralProgressTotalBy(1) 142 } 143 longPropertiesCount++ 144 } 145 } 146 if len(propertyQuery.Results) < propertyAqlPaginationLimit { 147 break 148 } 149 pageCounter++ 150 } 151 152 return longPropertiesCount 153 } 154 155 // Checks if the value of a property is not bigger than maxAllowedValLength 156 func isLongProperty(property Property) bool { 157 return property.valueLength() > maxAllowedValLength 158 } 159 160 // AqlPropertySearchResult - the structure that returns from a property aql search 161 type AqlPropertySearchResult struct { 162 Results []Property 163 } 164 165 // Get all the properties on the server using AQL (with pagination) 166 func runSearchPropertyAql(serviceManager artifactory.ArtifactoryServicesManager, pageNumber int) (result *AqlPropertySearchResult, err error) { 167 result = &AqlPropertySearchResult{} 168 err = runAqlService(serviceManager, getSearchAllPropertiesQuery(pageNumber), result) 169 return 170 } 171 172 // Get the query that search properties on a server with pagination 173 func getSearchAllPropertiesQuery(pageNumber int) string { 174 query := `properties.find()` 175 query += fmt.Sprintf(`.sort({"$asc":["key"]}).offset(%d).limit(%d)`, pageNumber*propertyAqlPaginationLimit, propertyAqlPaginationLimit) 176 return query 177 } 178 179 // Create a task that fetch from the server the files with the given property. 180 // We keep only the files that are at the requested repos and pass them at the files channel 181 func (lpc *LongPropertyCheck) createSearchPropertyTask(property Property, args precheckrunner.RunArguments, progress *progressbar.TasksProgressBar) parallel.TaskFunc { 182 return func(threadId int) (err error) { 183 serviceManager, err := utils.CreateServiceManagerWithContext(args.Context, args.ServerDetails, false, 0, retries, retriesWaitMilliSecs, propertiesRequestTimeout) 184 if err != nil { 185 return 186 } 187 // Search 188 var query *servicesUtils.AqlSearchResult 189 if query, err = lpc.runSearchPropertyInFilesAql(serviceManager, property); err != nil { 190 return 191 } 192 log.Debug(fmt.Sprintf("[Thread=%d] Got %d files from the query", threadId, len(query.Results))) 193 for _, item := range query.Results { 194 file := api.FileRepresentation{Repo: item.Repo, Path: item.Path, Name: item.Name} 195 // Keep only if in the requested repos 196 if slices.Contains(lpc.repos, file.Repo) { 197 fileWithLongProperty := FileWithLongProperty{file, property.valueLength(), property} 198 log.Debug(fmt.Sprintf("[Thread=%d] Found File{Repo=%s, Path=%s, Name=%s} with matching entry of long property.", threadId, file.Repo, file.Path, file.Name)) 199 lpc.filesChan <- fileWithLongProperty 200 } 201 } 202 // Notify end of search for the current property 203 if args.ProgressMng != nil && progress != nil { 204 progress.GetBar().Increment() 205 } 206 return 207 } 208 } 209 210 // Get all the files that contains the given property using AQL 211 func (lpc *LongPropertyCheck) runSearchPropertyInFilesAql(serviceManager artifactory.ArtifactoryServicesManager, property Property) (result *servicesUtils.AqlSearchResult, err error) { 212 result = &servicesUtils.AqlSearchResult{} 213 err = runAqlService(serviceManager, lpc.getSearchPropertyInFilesQuery(property), result) 214 return 215 } 216 217 // Get the query that search files with specific property 218 func (lpc *LongPropertyCheck) getSearchPropertyInFilesQuery(property Property) string { 219 query := fmt.Sprintf(`items.find({"type": {"$eq":"any"},"@%s":"%s"}).include("repo","path","name")`, property.Key, property.Value) 220 query += appendDistinctIfNeeded(lpc.disabledDistinctiveAql) 221 return query 222 } 223 224 // Run AQL service that return a result in the given format structure 'v' 225 func runAqlService(serviceManager artifactory.ArtifactoryServicesManager, query string, v any) (err error) { 226 reader, err := serviceManager.Aql(query) 227 if err != nil { 228 return 229 } 230 defer func() { 231 if reader != nil { 232 err = errors.Join(err, errorutils.CheckError(reader.Close())) 233 } 234 }() 235 respBody, err := io.ReadAll(reader) 236 if err != nil { 237 return errorutils.CheckError(err) 238 } 239 err = errorutils.CheckError(json.Unmarshal(respBody, v)) 240 return 241 } 242 243 // Create csv summary of all the files with long properties and log the result 244 func handleFailureRun(filesWithLongProperty []FileWithLongProperty) (err error) { 245 // Create summary 246 csvPath, err := cmdutils.CreateCSVFile("long-properties", filesWithLongProperty, time.Now()) 247 if err != nil { 248 log.Error("Couldn't create the long properties CSV file", err) 249 return 250 } 251 // Log result 252 nFails := len(filesWithLongProperty) 253 propertyTxt := "entries" 254 if nFails == 1 { 255 propertyTxt = "entry" 256 } 257 log.Info(fmt.Sprintf("Found %d property %s with value longer than 2.4k characters. Check the summary CSV file in: %s", nFails, propertyTxt, csvPath)) 258 return 259 }