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 }