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  }