github.com/jfrog/jfrog-client-go@v1.40.2/utils/utils.go (about)

     1  package utils
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"net/url"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/jfrog/jfrog-client-go/utils/io"
    17  
    18  	"github.com/jfrog/build-info-go/entities"
    19  	"github.com/jfrog/gofrog/stringutils"
    20  	"github.com/jfrog/gofrog/version"
    21  
    22  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    23  
    24  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    25  	"github.com/jfrog/jfrog-client-go/utils/log"
    26  )
    27  
    28  const (
    29  	Development = "development"
    30  	Agent       = "jfrog-client-go"
    31  	Version     = "1.40.2"
    32  )
    33  
    34  type MinVersionProduct string
    35  
    36  const (
    37  	Artifactory  MinVersionProduct = "JFrog Artifactory"
    38  	Xray         MinVersionProduct = "JFrog Xray"
    39  	Xsc          MinVersionProduct = "JFrog Xsc"
    40  	DataTransfer MinVersionProduct = "Data Transfer"
    41  	DockerApi    MinVersionProduct = "Docker API"
    42  	Projects     MinVersionProduct = "JFrog Projects"
    43  
    44  	MinimumVersionMsg = "You are using %s version %s, while this operation requires version %s or higher."
    45  )
    46  
    47  // In order to limit the number of items loaded from a reader into the memory, we use a buffers with this size limit.
    48  var (
    49  	MaxBufferSize          = 50000
    50  	userAgent              = getDefaultUserAgent()
    51  	curlyParenthesesRegexp = regexp.MustCompile(`\{(\d+?)}`)
    52  )
    53  
    54  func getVersion() string {
    55  	return Version
    56  }
    57  
    58  func GetUserAgent() string {
    59  	return userAgent
    60  }
    61  
    62  func SetUserAgent(newUserAgent string) {
    63  	userAgent = newUserAgent
    64  }
    65  
    66  func getDefaultUserAgent() string {
    67  	return fmt.Sprintf("%s/%s", Agent, getVersion())
    68  }
    69  
    70  func ValidateMinimumVersion(product MinVersionProduct, currentVersion, minimumVersion string) error {
    71  	if !version.NewVersion(currentVersion).AtLeast(minimumVersion) {
    72  		return errorutils.CheckErrorf(MinimumVersionMsg, product, currentVersion, minimumVersion)
    73  	}
    74  	return nil
    75  }
    76  
    77  // Get the local root path, from which to start collecting artifacts to be used for:
    78  // 1. Uploaded to Artifactory,
    79  // 2. Adding to the local build-info, to be later published to Artifactory.
    80  func GetRootPath(path string, patternType PatternType, parentheses ParenthesesSlice) string {
    81  	// The first step is to split the local path pattern into sections, by the file separator.
    82  	separator := "/"
    83  	sections := strings.Split(path, separator)
    84  	if len(sections) == 1 {
    85  		separator = "\\"
    86  		if strings.Contains(path, "\\\\") {
    87  			sections = strings.Split(path, "\\\\")
    88  		} else {
    89  			sections = strings.Split(path, separator)
    90  		}
    91  	}
    92  
    93  	// Now we start building the root path, making sure to leave out the sub-directory that includes the pattern.
    94  	rootPath := ""
    95  	for _, section := range sections {
    96  		if section == "" {
    97  			continue
    98  		}
    99  		if patternType == RegExp {
   100  			if strings.Contains(section, "(") {
   101  				break
   102  			}
   103  		} else {
   104  			if strings.Contains(section, "*") {
   105  				break
   106  			}
   107  			if strings.Contains(section, "(") {
   108  				temp := rootPath + section
   109  				if isWildcardParentheses(temp, parentheses) {
   110  					break
   111  				}
   112  			}
   113  			if patternType == AntPattern {
   114  				if strings.Contains(section, "?") {
   115  					break
   116  				}
   117  			}
   118  		}
   119  		if rootPath != "" {
   120  			rootPath += separator
   121  		}
   122  		if section == "~" {
   123  			rootPath += GetUserHomeDir()
   124  		} else {
   125  			rootPath += section
   126  		}
   127  	}
   128  	if len(sections) > 0 && sections[0] == "" {
   129  		rootPath = separator + rootPath
   130  	}
   131  	if rootPath == "" {
   132  		return "."
   133  	}
   134  	return rootPath
   135  }
   136  
   137  // Return true if the ‘str’ argument contains open parenthesis, that is related to a placeholder.
   138  // The ‘parentheses’ argument contains all the indexes of placeholder parentheses.
   139  func isWildcardParentheses(str string, parentheses ParenthesesSlice) bool {
   140  	toFind := "("
   141  	currStart := 0
   142  	for {
   143  		idx := strings.Index(str, toFind)
   144  		if idx == -1 {
   145  			break
   146  		}
   147  		if parentheses.IsPresent(idx) {
   148  			return true
   149  		}
   150  		currStart += idx + len(toFind)
   151  		str = str[idx+len(toFind):]
   152  	}
   153  	return false
   154  }
   155  
   156  func StringToBool(boolVal string, defaultValue bool) (bool, error) {
   157  	if len(boolVal) > 0 {
   158  		result, err := strconv.ParseBool(boolVal)
   159  		return result, errorutils.CheckError(err)
   160  	}
   161  	return defaultValue, nil
   162  }
   163  
   164  func AddTrailingSlashIfNeeded(url string) string {
   165  	if url != "" && !strings.HasSuffix(url, "/") {
   166  		url += "/"
   167  	}
   168  	return url
   169  }
   170  
   171  func IndentJson(jsonStr []byte) string {
   172  	return doIndentJson(jsonStr, "", "  ")
   173  }
   174  
   175  func IndentJsonArray(jsonStr []byte) string {
   176  	return doIndentJson(jsonStr, "  ", "  ")
   177  }
   178  
   179  func doIndentJson(jsonStr []byte, prefix, indent string) string {
   180  	var content bytes.Buffer
   181  	err := json.Indent(&content, jsonStr, prefix, indent)
   182  	if err == nil {
   183  		return content.String()
   184  	}
   185  	return string(jsonStr)
   186  }
   187  
   188  func MergeMaps(src map[string]string, dst map[string]string) {
   189  	for k, v := range src {
   190  		dst[k] = v
   191  	}
   192  }
   193  
   194  func CopyMap(src map[string]string) (dst map[string]string) {
   195  	dst = make(map[string]string)
   196  	for k, v := range src {
   197  		dst[k] = v
   198  	}
   199  	return
   200  }
   201  
   202  func ConvertLocalPatternToRegexp(localPath string, patternType PatternType) string {
   203  	if localPath == "./" || localPath == ".\\" || localPath == ".\\\\" {
   204  		return "^.*$"
   205  	}
   206  	localPath = strings.TrimPrefix(localPath, ".\\\\")
   207  	localPath = strings.TrimPrefix(localPath, "./")
   208  	localPath = strings.TrimPrefix(localPath, ".\\")
   209  
   210  	switch patternType {
   211  	case AntPattern:
   212  		localPath = AntToRegex(cleanPath(localPath))
   213  	case WildCardPattern:
   214  		localPath = stringutils.WildcardPatternToRegExp(cleanPath(localPath))
   215  	}
   216  
   217  	return localPath
   218  }
   219  
   220  // Clean /../ | /./ using filepath.Clean.
   221  func cleanPath(path string) string {
   222  	temp := path[len(path)-1:]
   223  	path = filepath.Clean(path)
   224  	if temp == `\` || temp == "/" {
   225  		path += temp
   226  	}
   227  	if io.IsWindows() {
   228  		// Since filepath.Clean replaces \\ with \, we revert this action.
   229  		path = strings.ReplaceAll(path, `\`, `\\`)
   230  		path = strings.ReplaceAll(path, `\\\\`, `\\`)
   231  	}
   232  	return path
   233  }
   234  
   235  // Builds a URL for Artifactory/Xray requests.
   236  // Pay attention: semicolons are escaped!
   237  func BuildUrl(baseUrl, path string, params map[string]string) (string, error) {
   238  	u := url.URL{Path: path}
   239  	parsedUrl, err := url.Parse(baseUrl + u.String())
   240  	if err = errorutils.CheckError(err); err != nil {
   241  		return "", err
   242  	}
   243  	q := parsedUrl.Query()
   244  	for k, v := range params {
   245  		q.Set(k, v)
   246  	}
   247  	parsedUrl.RawQuery = q.Encode()
   248  
   249  	// Semicolons are reserved as separators in some Artifactory APIs, so they'd better be encoded when used for other purposes
   250  	encodedUrl := strings.ReplaceAll(parsedUrl.String(), ";", url.QueryEscape(";"))
   251  	return encodedUrl, nil
   252  }
   253  
   254  // BuildTargetPath Replaces matched regular expression from path to corresponding placeholder {i} at target.
   255  // Example 1:
   256  //
   257  //	pattern = "repoA/1(.*)234" ; path = "repoA/1hello234" ; target = "{1}" ; ignoreRepo = false
   258  //	returns "hello"
   259  //
   260  // Example 2:
   261  //
   262  //	pattern = "repoA/1(.*)234" ; path = "repoB/1hello234" ; target = "{1}" ; ignoreRepo = true
   263  //	returns "hello"
   264  //
   265  // return (parsed target, placeholders replaced in target, error)
   266  func BuildTargetPath(pattern, path, target string, ignoreRepo bool) (string, bool, error) {
   267  	asteriskIndex := strings.Index(pattern, "*")
   268  	slashIndex := strings.Index(pattern, "/")
   269  	if shouldRemoveRepo(ignoreRepo, asteriskIndex, slashIndex) {
   270  		// Removing the repository part of the path is required when working with virtual repositories, as the pattern
   271  		// may contain the virtual-repository name, but the path contains the local-repository name.
   272  		pattern = removeRepoFromPath(pattern)
   273  		path = removeRepoFromPath(path)
   274  	}
   275  	pattern = addEscapingParentheses(pattern, target)
   276  	pattern = stringutils.WildcardPatternToRegExp(pattern)
   277  	if slashIndex < 0 {
   278  		// If '/' doesn't exist, add an optional trailing-slash to support cases in which the provided pattern
   279  		// is only the repository name.
   280  		dollarIndex := strings.LastIndex(pattern, "$")
   281  		pattern = pattern[:dollarIndex]
   282  		pattern += "(/.*)?$"
   283  	}
   284  
   285  	r, err := regexp.Compile(pattern)
   286  	err = errorutils.CheckError(err)
   287  	if err != nil {
   288  		return "", false, err
   289  	}
   290  
   291  	groups := r.FindStringSubmatch(path)
   292  	if len(groups) > 0 {
   293  		target, replaceOccurred, err := ReplacePlaceHolders(groups, target, false)
   294  		if err != nil {
   295  			return "", false, err
   296  		}
   297  		return target, replaceOccurred, nil
   298  	}
   299  	return target, false, nil
   300  }
   301  
   302  // ReplacePlaceHolders replace placeholders with their matching regular expressions.
   303  // group - Regular expression matched group to replace with placeholders.
   304  // toReplace - Target pattern to replace.
   305  // isRegexp - When using a regular expression, all parentheses content in the target will be at the given group parameter.
   306  // A non-regular expression will, however, allow us to consider the parentheses as literal characters.
   307  // The size of the group (containing the parentheses content) can be smaller than the maximum placeholder indexer - in this case, special treatment is required.
   308  // Example : pattern: (a)/(b)/(c), target: "target/{1}{3}" => '(a)' and '(c)' will be considered as placeholders, and '(b)' will be treated as the directory's actual name.
   309  // In this case, the index of '(c)' in the group is 2, but its placeholder indexer is 3.
   310  // Return - The parsed placeholders string, along with a boolean to indicate whether they have been replaced or not.
   311  func ReplacePlaceHolders(groups []string, toReplace string, isRegexp bool) (string, bool, error) {
   312  	maxPlaceholderIndex, err := getMaxPlaceholderIndex(toReplace)
   313  	if err != nil {
   314  		return "", false, err
   315  	}
   316  	preReplaced := toReplace
   317  	// Index for the placeholder number.
   318  	placeHolderIndexer := 1
   319  	for i := 1; i < len(groups); i++ {
   320  		group := strings.ReplaceAll(groups[i], "\\", "/")
   321  		// Handling non-regular expression cases
   322  		for !isRegexp && !strings.Contains(toReplace, "{"+strconv.Itoa(placeHolderIndexer)+"}") {
   323  			placeHolderIndexer++
   324  			if placeHolderIndexer > maxPlaceholderIndex {
   325  				break
   326  			}
   327  		}
   328  		toReplace = strings.ReplaceAll(toReplace, "{"+strconv.Itoa(placeHolderIndexer)+"}", group)
   329  		placeHolderIndexer++
   330  	}
   331  	replaceOccurred := preReplaced != toReplace
   332  	return toReplace, replaceOccurred, nil
   333  }
   334  
   335  // Returns the higher index between all placeHolders target instances.
   336  // Example: for input "{1}{5}{3}" returns 5.
   337  func getMaxPlaceholderIndex(toReplace string) (int, error) {
   338  	placeholders := curlyParenthesesRegexp.FindAllString(toReplace, -1)
   339  	max := 0
   340  	for _, placeholder := range placeholders {
   341  		num, err := strconv.Atoi(strings.TrimPrefix(strings.TrimSuffix(placeholder, "}"), "{"))
   342  		if err != nil {
   343  			return 0, errorutils.CheckError(err)
   344  		}
   345  		if num > max {
   346  			max = num
   347  		}
   348  	}
   349  	return max, nil
   350  }
   351  
   352  func GetLogMsgPrefix(threadId int, dryRun bool) string {
   353  	var strDryRun string
   354  	if dryRun {
   355  		strDryRun = "[Dry run] "
   356  	}
   357  	return "[Thread " + strconv.Itoa(threadId) + "] " + strDryRun
   358  }
   359  
   360  func TrimPath(path string) string {
   361  	path = strings.ReplaceAll(path, "\\", "/")
   362  	path = strings.ReplaceAll(path, "//", "/")
   363  	path = strings.ReplaceAll(path, "../", "")
   364  	path = strings.ReplaceAll(path, "./", "")
   365  	return path
   366  }
   367  
   368  func Bool2Int(b bool) int {
   369  	if b {
   370  		return 1
   371  	}
   372  	return 0
   373  }
   374  
   375  func ReplaceTildeWithUserHome(path string) string {
   376  	if len(path) > 1 && path[0:1] == "~" {
   377  		return GetUserHomeDir() + path[1:]
   378  	}
   379  	return path
   380  }
   381  
   382  func GetUserHomeDir() string {
   383  	if io.IsWindows() {
   384  		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
   385  		if home == "" {
   386  			home = os.Getenv("USERPROFILE")
   387  		}
   388  		return strings.ReplaceAll(home, "\\", "\\\\")
   389  	}
   390  	return os.Getenv("HOME")
   391  }
   392  
   393  func GetBoolEnvValue(flagName string, defValue bool) (bool, error) {
   394  	envVarValue := os.Getenv(flagName)
   395  	if envVarValue == "" {
   396  		return defValue, nil
   397  	}
   398  	val, err := strconv.ParseBool(envVarValue)
   399  	err = CheckErrorWithMessage(err, "can't parse environment variable "+flagName)
   400  	return val, err
   401  }
   402  
   403  func CheckErrorWithMessage(err error, message string) error {
   404  	if err != nil {
   405  		log.Error(message)
   406  		err = errorutils.CheckError(err)
   407  	}
   408  	return err
   409  }
   410  
   411  func ConvertSliceToMap(slice []string) map[string]bool {
   412  	mapFromSlice := make(map[string]bool)
   413  	for _, value := range slice {
   414  		mapFromSlice[value] = true
   415  	}
   416  	return mapFromSlice
   417  }
   418  
   419  func removeRepoFromPath(path string) string {
   420  	if idx := strings.Index(path, "/"); idx != -1 {
   421  		return path[idx:]
   422  	}
   423  	return path
   424  }
   425  
   426  func shouldRemoveRepo(ignoreRepo bool, asteriskIndex, slashIndex int) bool {
   427  	if !ignoreRepo || slashIndex < 0 {
   428  		return false
   429  	}
   430  	if asteriskIndex < 0 {
   431  		return true
   432  	}
   433  	return IsSlashPrecedeAsterisk(asteriskIndex, slashIndex)
   434  }
   435  
   436  func IsSlashPrecedeAsterisk(asteriskIndex, slashIndex int) bool {
   437  	return slashIndex < asteriskIndex && slashIndex >= 0
   438  }
   439  
   440  // Split str by the provided separator, escaping the separator if it is prefixed by a back-slash.
   441  func SplitWithEscape(str string, separator rune) []string {
   442  	var parts []string
   443  	var current bytes.Buffer
   444  	escaped := false
   445  	for _, char := range str {
   446  		switch {
   447  		case char == '\\':
   448  			if escaped {
   449  				current.WriteRune(char)
   450  			}
   451  			escaped = true
   452  		case char == separator && !escaped:
   453  			parts = append(parts, current.String())
   454  			current.Reset()
   455  		default:
   456  			escaped = false
   457  			current.WriteRune(char)
   458  		}
   459  	}
   460  	parts = append(parts, current.String())
   461  	return parts
   462  }
   463  
   464  func AddProps(oldProps, additionalProps string) string {
   465  	if len(oldProps) > 0 && !strings.HasSuffix(oldProps, ";") && len(additionalProps) > 0 {
   466  		oldProps += ";"
   467  	}
   468  	return oldProps + additionalProps
   469  }
   470  
   471  type Artifact struct {
   472  	LocalPath           string
   473  	TargetPath          string
   474  	SymlinkTargetPath   string
   475  	TargetPathInArchive string
   476  }
   477  
   478  const (
   479  	WildCardPattern PatternType = "wildcard"
   480  	RegExp          PatternType = "regexp"
   481  	AntPattern      PatternType = "ant"
   482  )
   483  
   484  type PatternType string
   485  
   486  type PatternTypes struct {
   487  	RegExp bool
   488  	Ant    bool
   489  }
   490  
   491  func GetPatternType(patternTypes PatternTypes) PatternType {
   492  	if patternTypes.RegExp {
   493  		return RegExp
   494  	}
   495  	if patternTypes.Ant {
   496  		return AntPattern
   497  	}
   498  	return WildCardPattern
   499  }
   500  
   501  type Sha256Summary struct {
   502  	sha256    string
   503  	succeeded bool
   504  }
   505  
   506  func NewSha256Summary() *Sha256Summary {
   507  	return &Sha256Summary{}
   508  }
   509  
   510  func (bps *Sha256Summary) IsSucceeded() bool {
   511  	return bps.succeeded
   512  }
   513  
   514  func (bps *Sha256Summary) SetSucceeded(succeeded bool) *Sha256Summary {
   515  	bps.succeeded = succeeded
   516  	return bps
   517  }
   518  
   519  func (bps *Sha256Summary) GetSha256() string {
   520  	return bps.sha256
   521  }
   522  
   523  func (bps *Sha256Summary) SetSha256(sha256 string) *Sha256Summary {
   524  	bps.sha256 = sha256
   525  	return bps
   526  }
   527  
   528  // Represents a file transfer from SourcePath to TargetPath.
   529  // Each of the paths can be on the local machine (full or relative) or in Artifactory (without Artifactory URL).
   530  // The file's Sha256 is calculated by Artifactory during the upload. we read the sha256 from the HTTP's response body.
   531  type FileTransferDetails struct {
   532  	SourcePath string `json:"sourcePath,omitempty"`
   533  	TargetPath string `json:"targetPath,omitempty"`
   534  	RtUrl      string `json:"rtUrl,omitempty"`
   535  	Sha256     string `json:"sha256,omitempty"`
   536  }
   537  
   538  // Represent deployed artifact's details returned from build-info project for maven and gradle.
   539  type DeployableArtifactDetails struct {
   540  	SourcePath       string `json:"sourcePath,omitempty"`
   541  	ArtifactDest     string `json:"artifactDest,omitempty"`
   542  	Sha256           string `json:"sha256,omitempty"`
   543  	DeploySucceeded  bool   `json:"deploySucceeded,omitempty"`
   544  	TargetRepository string `json:"targetRepository,omitempty"`
   545  }
   546  
   547  func (details *DeployableArtifactDetails) CreateFileTransferDetails(rtUrl, targetRepository string) (FileTransferDetails, error) {
   548  	targetUrl, err := url.Parse(path.Join(targetRepository, details.ArtifactDest))
   549  	if err != nil {
   550  		return FileTransferDetails{}, err
   551  	}
   552  	return FileTransferDetails{SourcePath: details.SourcePath, TargetPath: targetUrl.String(), Sha256: details.Sha256, RtUrl: rtUrl}, nil
   553  }
   554  
   555  type UploadResponseBody struct {
   556  	Checksums entities.Checksum `json:"checksums,omitempty"`
   557  }
   558  
   559  func SaveFileTransferDetailsInTempFile(filesDetails *[]FileTransferDetails) (filePath string, err error) {
   560  	tempFile, err := fileutils.CreateTempFile()
   561  	if err != nil {
   562  		return "", err
   563  	}
   564  	defer func() {
   565  		err = errors.Join(err, errorutils.CheckError(tempFile.Close()))
   566  	}()
   567  
   568  	filePath = tempFile.Name()
   569  	return filePath, SaveFileTransferDetailsInFile(filePath, filesDetails)
   570  }
   571  
   572  func SaveFileTransferDetailsInFile(filePath string, details *[]FileTransferDetails) error {
   573  	// Marshal and save files details to a file.
   574  	// The details will be saved in a json format in an array with key "files" for printing later
   575  	finalResult := struct {
   576  		Files *[]FileTransferDetails `json:"files"`
   577  	}{}
   578  	finalResult.Files = details
   579  	files, err := json.Marshal(finalResult)
   580  	if err != nil {
   581  		return errorutils.CheckError(err)
   582  	}
   583  	return errorutils.CheckError(os.WriteFile(filePath, files, 0700))
   584  }
   585  
   586  // Extract sha256 of the uploaded file (calculated by artifactory) from the response's body.
   587  // In case of uploading archive with "--explode" the response body will be empty and sha256 won't be shown at
   588  // the detailed summary.
   589  func ExtractSha256FromResponseBody(body []byte) (string, error) {
   590  	if len(body) > 0 {
   591  		responseBody := new(UploadResponseBody)
   592  		err := json.Unmarshal(body, &responseBody)
   593  		if errorutils.CheckError(err) != nil {
   594  			return "", err
   595  		}
   596  		return responseBody.Checksums.Sha256, nil
   597  	}
   598  	return "", nil
   599  }
   600  
   601  // Convert any value to a pointer to that value
   602  func Pointer[K any](val K) *K {
   603  	return &val
   604  }
   605  
   606  func SetEnvWithResetCallback(key, value string) (func() error, error) {
   607  	oldValue, exist := os.LookupEnv(key)
   608  	if err := os.Setenv(key, value); err != nil {
   609  		return func() error { return nil }, errorutils.CheckError(err)
   610  	}
   611  	if exist {
   612  		return func() error {
   613  			return errorutils.CheckError(os.Setenv(key, oldValue))
   614  		}, nil
   615  	}
   616  	return func() error {
   617  		return errorutils.CheckError(os.Unsetenv(key))
   618  	}, nil
   619  }