github.com/cobalt77/jfrog-client-go@v0.14.5/artifactory/services/utils/searchutil.go (about) 1 package utils 2 3 import ( 4 "bufio" 5 "errors" 6 "io" 7 "net/http" 8 "os" 9 "path" 10 "sort" 11 "strconv" 12 "strings" 13 14 "github.com/cobalt77/jfrog-client-go/artifactory/buildinfo" 15 "github.com/cobalt77/jfrog-client-go/utils" 16 "github.com/cobalt77/jfrog-client-go/utils/errorutils" 17 "github.com/cobalt77/jfrog-client-go/utils/io/content" 18 "github.com/cobalt77/jfrog-client-go/utils/io/fileutils" 19 "github.com/cobalt77/jfrog-client-go/utils/log" 20 ) 21 22 type RequiredArtifactProps int 23 24 // This enum defines which properties are required in the result of the aql. 25 // For example, when performing a copy/move command - the props are not needed, so we set RequiredArtifactProps to NONE. 26 const ( 27 ALL RequiredArtifactProps = iota 28 SYMLINK 29 NONE 30 ) 31 32 // Use this function when searching by build without pattern or aql. 33 // Search with builds returns many results, some are not part of the build and others may be duplicated of the same artifact. 34 // 1. Save SHA1 values received for build-name. 35 // 2. Remove artifacts that not are present on the sha1 list 36 // 3. If we have more than one artifact with the same sha1: 37 // 3.1 Compare the build-name & build-number among all the artifact with the same sha1. 38 // This will prevent unnecessary search upon all Artifactory: 39 func SearchBySpecWithBuild(specFile *ArtifactoryCommonParams, flags CommonConf) (*content.ContentReader, error) { 40 buildName, buildNumber, err := getBuildNameAndNumberFromBuildIdentifier(specFile.Build, flags) 41 if err != nil { 42 return nil, err 43 } 44 specFile.Aql = Aql{ItemsFind: createAqlBodyForBuild(buildName, buildNumber)} 45 executionQuery := BuildQueryFromSpecFile(specFile, ALL) 46 reader, err := aqlSearch(executionQuery, flags) 47 if err != nil { 48 return nil, err 49 } 50 defer reader.Close() 51 52 // If artifacts' properties weren't fetched in previous aql, fetch now and add to results. 53 if !includePropertiesInAqlForSpec(specFile) { 54 readerWithProps, err := searchProps(specFile.Aql.ItemsFind, "build.name", buildName, flags) 55 if err != nil { 56 return nil, err 57 } 58 defer readerWithProps.Close() 59 readerSortedWithProps, err := loadMissingProperties(reader, readerWithProps) 60 if err != nil { 61 return nil, err 62 } 63 buildArtifactsSha1, err := extractSha1FromAqlResponse(readerSortedWithProps) 64 return filterBuildAqlSearchResults(readerSortedWithProps, buildArtifactsSha1, buildName, buildNumber) 65 } 66 67 buildArtifactsSha1, err := extractSha1FromAqlResponse(reader) 68 return filterBuildAqlSearchResults(reader, buildArtifactsSha1, buildName, buildNumber) 69 } 70 71 // Perform search by pattern. 72 func SearchBySpecWithPattern(specFile *ArtifactoryCommonParams, flags CommonConf, requiredArtifactProps RequiredArtifactProps) (*content.ContentReader, error) { 73 // Create AQL according to spec fields. 74 query, err := CreateAqlBodyForSpecWithPattern(specFile) 75 if err != nil { 76 return nil, err 77 } 78 specFile.Aql = Aql{ItemsFind: query} 79 return SearchBySpecWithAql(specFile, flags, requiredArtifactProps) 80 } 81 82 // Use this function when running Aql with pattern 83 func SearchBySpecWithAql(specFile *ArtifactoryCommonParams, flags CommonConf, requiredArtifactProps RequiredArtifactProps) (*content.ContentReader, error) { 84 // Execute the search according to provided aql in specFile. 85 var fetchedProps *content.ContentReader 86 query := BuildQueryFromSpecFile(specFile, requiredArtifactProps) 87 reader, err := aqlSearch(query, flags) 88 if err != nil { 89 return nil, err 90 } 91 filteredReader, err := FilterResultsByBuild(specFile, flags, requiredArtifactProps, reader) 92 if err != nil { 93 return nil, err 94 } 95 if filteredReader != nil { 96 defer reader.Close() 97 fetchedProps, err = fetchProps(specFile, flags, requiredArtifactProps, filteredReader) 98 if fetchedProps != nil { 99 defer filteredReader.Close() 100 return fetchedProps, err 101 } 102 return filteredReader, err 103 } 104 fetchedProps, err = fetchProps(specFile, flags, requiredArtifactProps, reader) 105 if fetchedProps != nil { 106 defer reader.Close() 107 return fetchedProps, err 108 } 109 return reader, err 110 } 111 112 // Filter the results by build, if no build found or items to filter, nil will be returned. 113 func FilterResultsByBuild(specFile *ArtifactoryCommonParams, flags CommonConf, requiredArtifactProps RequiredArtifactProps, reader *content.ContentReader) (*content.ContentReader, error) { 114 length, err := reader.Length() 115 if err != nil { 116 return nil, err 117 } 118 if specFile.Build != "" && length > 0 { 119 // If requiredArtifactProps is not NONE and 'includePropertiesInAqlForSpec' for specFile returned true, results contains properties for artifacts. 120 resultsArtifactsIncludeProperties := requiredArtifactProps != NONE && includePropertiesInAqlForSpec(specFile) 121 return filterAqlSearchResultsByBuild(specFile, reader, flags, resultsArtifactsIncludeProperties) 122 } 123 return nil, nil 124 } 125 126 // Fetch properties only if: 127 // 1. Properties weren't included in 'results'. 128 // AND 129 // 2. Properties weren't fetched during 'build' filtering 130 // Otherwise, nil will be returned 131 func fetchProps(specFile *ArtifactoryCommonParams, flags CommonConf, requiredArtifactProps RequiredArtifactProps, reader *content.ContentReader) (*content.ContentReader, error) { 132 if !includePropertiesInAqlForSpec(specFile) && specFile.Build == "" && requiredArtifactProps != NONE { 133 var readerWithProps *content.ContentReader 134 var err error 135 switch requiredArtifactProps { 136 case ALL: 137 readerWithProps, err = searchProps(specFile.Aql.ItemsFind, "*", "*", flags) 138 case SYMLINK: 139 readerWithProps, err = searchProps(specFile.Aql.ItemsFind, "symlink.dest", "*", flags) 140 } 141 if err != nil { 142 return nil, err 143 } 144 defer readerWithProps.Close() 145 return loadMissingProperties(reader, readerWithProps) 146 } 147 return nil, nil 148 } 149 150 func aqlSearch(aqlQuery string, flags CommonConf) (*content.ContentReader, error) { 151 return ExecAqlSaveToFile(aqlQuery, flags) 152 } 153 154 func ExecAql(aqlQuery string, flags CommonConf) (io.ReadCloser, error) { 155 client, err := flags.GetJfrogHttpClient() 156 if err != nil { 157 return nil, err 158 } 159 aqlUrl := flags.GetArtifactoryDetails().GetUrl() + "api/search/aql" 160 log.Debug("Searching Artifactory using AQL query:\n", aqlQuery) 161 httpClientsDetails := flags.GetArtifactoryDetails().CreateHttpClientDetails() 162 resp, err := client.SendPostLeaveBodyOpen(aqlUrl, []byte(aqlQuery), &httpClientsDetails) 163 if err != nil { 164 return nil, err 165 } 166 if resp.StatusCode != http.StatusOK { 167 return nil, errorutils.CheckError(errors.New("Artifactory response: " + resp.Status + "\n")) 168 } 169 log.Debug("Artifactory response: ", resp.Status) 170 return resp.Body, err 171 } 172 173 func ExecAqlSaveToFile(aqlQuery string, flags CommonConf) (*content.ContentReader, error) { 174 body, err := ExecAql(aqlQuery, flags) 175 if err != nil { 176 return nil, err 177 } 178 defer func() { 179 err := body.Close() 180 if err != nil { 181 log.Warn("Could not close connection:" + err.Error() + ".") 182 } 183 }() 184 log.Debug("Streaming data to file...") 185 filePath, err := streamToFile(body) 186 if err != nil { 187 return nil, err 188 } 189 log.Debug("Finish streaming data successfully.") 190 return content.NewContentReader(filePath, content.DefaultKey), err 191 } 192 193 // Save the reader output into a temp file. 194 // return the file path. 195 func streamToFile(reader io.Reader) (string, error) { 196 var fd *os.File 197 bufio := bufio.NewReaderSize(reader, 65536) 198 fd, err := fileutils.CreateTempFile() 199 if err != nil { 200 return "", err 201 } 202 defer fd.Close() 203 _, err = io.Copy(fd, bufio) 204 return fd.Name(), errorutils.CheckError(err) 205 } 206 207 func LogSearchResults(numOfArtifacts int) { 208 var msgSuffix = "artifacts." 209 if numOfArtifacts == 1 { 210 msgSuffix = "artifact." 211 } 212 log.Info("Found", strconv.Itoa(numOfArtifacts), msgSuffix) 213 } 214 215 type AqlSearchResult struct { 216 Results []ResultItem 217 } 218 219 type ResultItem struct { 220 Repo string `json:"repo,omitempty"` 221 Path string `json:"path,omitempty"` 222 Name string `json:"name,omitempty"` 223 Actual_Md5 string `json:"actual_md5,omitempty"` 224 Actual_Sha1 string `json:"actual_sha1,omitempty"` 225 Size int64 `json:"size,omitempty"` 226 Created string `json:"created,omitempty"` 227 Modified string `json:"modified,omitempty"` 228 Properties []Property `json:"properties,omitempty"` 229 Type string `json:"type,omitempty"` 230 } 231 232 func (item ResultItem) GetItemRelativePath() string { 233 if item.Path == "." { 234 return path.Join(item.Repo, item.Name) 235 } 236 237 url := item.Repo 238 url = addSeparator(url, "/", item.Path) 239 url = addSeparator(url, "/", item.Name) 240 if item.Type == "folder" && !strings.HasSuffix(url, "/") { 241 url = url + "/" 242 } 243 return url 244 } 245 246 // Returns "item.Repo/item.Path/" lowercased. 247 func (item ResultItem) GetItemRelativeLocation() string { 248 return strings.ToLower(addSeparator(item.Repo, "/", item.Path) + "/") 249 } 250 251 func addSeparator(str1, separator, str2 string) string { 252 if str2 == "" { 253 return str1 254 } 255 if str1 == "" { 256 return str2 257 } 258 259 return str1 + separator + str2 260 } 261 262 func (item *ResultItem) ToArtifact() buildinfo.Artifact { 263 return buildinfo.Artifact{Name: item.Name, Checksum: &buildinfo.Checksum{Sha1: item.Actual_Sha1, Md5: item.Actual_Md5}, Path: path.Join(item.Repo, item.Path, item.Name)} 264 } 265 266 func (item *ResultItem) ToDependency() buildinfo.Dependency { 267 return buildinfo.Dependency{Id: item.Name, Checksum: &buildinfo.Checksum{Sha1: item.Actual_Sha1, Md5: item.Actual_Md5}} 268 } 269 270 type AqlSearchResultItemFilter func(*content.ContentReader) (*content.ContentReader, error) 271 272 func FilterBottomChainResults(reader *content.ContentReader) (*content.ContentReader, error) { 273 writer, err := content.NewContentWriter(content.DefaultKey, true, false) 274 if err != nil { 275 return nil, err 276 } 277 defer writer.Close() 278 var temp string 279 for resultItem := new(ResultItem); reader.NextRecord(resultItem) == nil; resultItem = new(ResultItem) { 280 rPath := resultItem.GetItemRelativePath() 281 if !strings.HasSuffix(rPath, "/") { 282 rPath += "/" 283 } 284 if temp == "" || !strings.HasPrefix(temp, rPath) { 285 writer.Write(*resultItem) 286 temp = rPath 287 } 288 } 289 if err := reader.GetError(); err != nil { 290 return nil, err 291 } 292 reader.Reset() 293 return content.NewContentReader(writer.GetFilePath(), writer.GetArrayKey()), nil 294 } 295 296 // Reduce the amount of items by saveing only the shortest item path for each unique path e.g.: 297 // a | a/b | c | e/f -> a | c | e/f 298 func FilterTopChainResults(reader *content.ContentReader) (*content.ContentReader, error) { 299 writer, err := content.NewContentWriter(content.DefaultKey, true, false) 300 if err != nil { 301 return nil, err 302 } 303 defer writer.Close() 304 var prevFolder string 305 for resultItem := new(ResultItem); reader.NextRecord(resultItem) == nil; resultItem = new(ResultItem) { 306 rPath := resultItem.GetItemRelativePath() 307 if resultItem.Type == "folder" && !strings.HasSuffix(rPath, "/") { 308 rPath += "/" 309 } 310 if prevFolder == "" || !strings.HasPrefix(rPath, prevFolder) { 311 writer.Write(*resultItem) 312 if resultItem.Type == "folder" { 313 prevFolder = rPath 314 } 315 } 316 } 317 if err := reader.GetError(); err != nil { 318 return nil, err 319 } 320 reader.Reset() 321 return content.NewContentReader(writer.GetFilePath(), writer.GetArrayKey()), nil 322 } 323 324 func ReduceTopChainDirResult(searchResults *content.ContentReader) (*content.ContentReader, error) { 325 return ReduceDirResult(searchResults, true, FilterTopChainResults) 326 } 327 328 func ReduceBottomChainDirResult(searchResults *content.ContentReader) (*content.ContentReader, error) { 329 return ReduceDirResult(searchResults, false, FilterBottomChainResults) 330 } 331 332 // Reduce Dir results by using the resultsFilter 333 func ReduceDirResult(searchResults *content.ContentReader, ascendingOrder bool, resultsFilter AqlSearchResultItemFilter) (*content.ContentReader, error) { 334 // Sort results in asc order according to relative path. 335 // Split to files if the total result is bigget than the maximum buffest. 336 paths := make(map[string]ResultItem) 337 pathsKeys := make([]string, 0, utils.MaxBufferSize) 338 sortedFiles := []*content.ContentReader{} 339 defer func() { 340 for _, file := range sortedFiles { 341 file.Close() 342 } 343 }() 344 for resultItem := new(ResultItem); searchResults.NextRecord(resultItem) == nil; resultItem = new(ResultItem) { 345 if resultItem.Name == "." { 346 continue 347 } 348 rPath := resultItem.GetItemRelativePath() 349 paths[rPath] = *resultItem 350 pathsKeys = append(pathsKeys, rPath) 351 if len(pathsKeys) == utils.MaxBufferSize { 352 sortedFile, err := SortAndSaveBufferToFile(paths, pathsKeys, ascendingOrder) 353 if err != nil { 354 return nil, err 355 } 356 sortedFiles = append(sortedFiles, sortedFile) 357 paths = make(map[string]ResultItem) 358 pathsKeys = make([]string, 0, utils.MaxBufferSize) 359 } 360 } 361 if err := searchResults.GetError(); err != nil { 362 return nil, err 363 } 364 searchResults.Reset() 365 var sortedFile *content.ContentReader 366 if len(pathsKeys) > 0 { 367 sortedFile, err := SortAndSaveBufferToFile(paths, pathsKeys, ascendingOrder) 368 if err != nil { 369 return nil, err 370 } 371 sortedFiles = append(sortedFiles, sortedFile) 372 } 373 // Merge sorted files 374 sortedFile, err := MergeSortedFiles(sortedFiles, ascendingOrder) 375 if err != nil { 376 return nil, err 377 } 378 defer sortedFile.Close() 379 return resultsFilter(sortedFile) 380 } 381 382 func SortAndSaveBufferToFile(paths map[string]ResultItem, pathsKeys []string, increasingOrder bool) (*content.ContentReader, error) { 383 if len(pathsKeys) == 0 { 384 return nil, nil 385 } 386 writer, err := content.NewContentWriter(content.DefaultKey, true, false) 387 if err != nil { 388 return nil, err 389 } 390 defer writer.Close() 391 if increasingOrder { 392 sort.Strings(pathsKeys) 393 } else { 394 sort.Sort(sort.Reverse(sort.StringSlice(pathsKeys))) 395 } 396 for _, v := range pathsKeys { 397 writer.Write(paths[v]) 398 } 399 return content.NewContentReader(writer.GetFilePath(), writer.GetArrayKey()), nil 400 } 401 402 // Merge all the sorted files into a single sorted file. 403 func MergeSortedFiles(sortedFiles []*content.ContentReader, ascendingOrder bool) (*content.ContentReader, error) { 404 if len(sortedFiles) == 0 { 405 return content.NewEmptyContentReader(content.DefaultKey), nil 406 } 407 resultWriter, err := content.NewContentWriter(content.DefaultKey, true, false) 408 if err != nil { 409 return nil, err 410 } 411 defer resultWriter.Close() 412 currentResultItem := make([]*ResultItem, len(sortedFiles)) 413 sortedFilesClone := make([]*content.ContentReader, len(sortedFiles)) 414 copy(sortedFilesClone, sortedFiles) 415 for { 416 var candidateToWrite *ResultItem 417 smallestIndex := 0 418 for i := 0; i < len(sortedFilesClone); i++ { 419 if currentResultItem[i] == nil && sortedFilesClone[i] != nil { 420 temp := new(ResultItem) 421 if err := sortedFilesClone[i].NextRecord(temp); nil != err { 422 sortedFilesClone[i] = nil 423 continue 424 } 425 currentResultItem[i] = temp 426 } 427 if candidateToWrite == nil || (currentResultItem[i] != nil && compareStrings(candidateToWrite.GetItemRelativePath(), currentResultItem[i].GetItemRelativePath(), ascendingOrder)) { 428 candidateToWrite = currentResultItem[i] 429 smallestIndex = i 430 } 431 } 432 if candidateToWrite == nil { 433 break 434 } 435 resultWriter.Write(*candidateToWrite) 436 currentResultItem[smallestIndex] = nil 437 } 438 return content.NewContentReader(resultWriter.GetFilePath(), resultWriter.GetArrayKey()), nil 439 } 440 441 func compareStrings(src, against string, ascendingOrder bool) bool { 442 if ascendingOrder { 443 return src > against 444 } 445 return src < against 446 }