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