github.com/cobalt77/jfrog-client-go@v0.14.5/artifactory/services/upload.go (about) 1 package services 2 3 import ( 4 "net/http" 5 "os" 6 "path/filepath" 7 "regexp" 8 "sort" 9 "strconv" 10 "strings" 11 12 rthttpclient "github.com/cobalt77/jfrog-client-go/artifactory/httpclient" 13 "github.com/cobalt77/jfrog-client-go/artifactory/services/fspatterns" 14 "github.com/cobalt77/jfrog-client-go/artifactory/services/utils" 15 "github.com/cobalt77/jfrog-client-go/auth" 16 clientutils "github.com/cobalt77/jfrog-client-go/utils" 17 "github.com/cobalt77/jfrog-client-go/utils/errorutils" 18 ioutils "github.com/cobalt77/jfrog-client-go/utils/io" 19 "github.com/cobalt77/jfrog-client-go/utils/io/fileutils" 20 "github.com/cobalt77/jfrog-client-go/utils/io/fileutils/checksum" 21 "github.com/cobalt77/jfrog-client-go/utils/io/httputils" 22 "github.com/cobalt77/jfrog-client-go/utils/log" 23 "github.com/jfrog/gofrog/parallel" 24 ) 25 26 type UploadService struct { 27 client *rthttpclient.ArtifactoryHttpClient 28 Progress ioutils.Progress 29 ArtDetails auth.ServiceDetails 30 DryRun bool 31 Threads int 32 } 33 34 func NewUploadService(client *rthttpclient.ArtifactoryHttpClient) *UploadService { 35 return &UploadService{client: client} 36 } 37 38 func (us *UploadService) SetThreads(threads int) { 39 us.Threads = threads 40 } 41 42 func (us *UploadService) GetJfrogHttpClient() *rthttpclient.ArtifactoryHttpClient { 43 return us.client 44 } 45 46 func (us *UploadService) SetServiceDetails(artDetails auth.ServiceDetails) { 47 us.ArtDetails = artDetails 48 } 49 50 func (us *UploadService) SetDryRun(isDryRun bool) { 51 us.DryRun = isDryRun 52 } 53 54 func (us *UploadService) UploadFiles(uploadParams ...UploadParams) (artifactsFileInfo []utils.FileInfo, totalUploaded, totalFailed int, err error) { 55 // Uploading threads are using this struct to report upload results. 56 uploadSummary := *utils.NewUploadResult(us.Threads) 57 producerConsumer := parallel.NewRunner(us.Threads, 100, false) 58 errorsQueue := clientutils.NewErrorsQueue(1) 59 us.prepareUploadTasks(producerConsumer, errorsQueue, uploadSummary, uploadParams...) 60 return us.performUploadTasks(producerConsumer, &uploadSummary, errorsQueue) 61 } 62 63 func (us *UploadService) prepareUploadTasks(producer parallel.Runner, errorsQueue *clientutils.ErrorsQueue, uploadSummary utils.UploadResult, uploadParamsSlice ...UploadParams) { 64 go func() { 65 defer producer.Done() 66 // Iterate over file-spec groups and produce upload tasks. 67 // When encountering an error, log and move to next group. 68 vcsCache := clientutils.NewVcsDetals() 69 for _, uploadParams := range uploadParamsSlice { 70 artifactHandlerFunc := us.createArtifactHandlerFunc(&uploadSummary, uploadParams) 71 err := collectFilesForUpload(uploadParams, producer, artifactHandlerFunc, errorsQueue, vcsCache) 72 if err != nil { 73 log.Error(err) 74 errorsQueue.AddError(err) 75 } 76 } 77 }() 78 } 79 80 func (us *UploadService) performUploadTasks(consumer parallel.Runner, uploadSummary *utils.UploadResult, errorsQueue *clientutils.ErrorsQueue) (artifactsFileInfo []utils.FileInfo, totalUploaded, totalFailed int, err error) { 81 // Blocking until consuming is finished. 82 consumer.Run() 83 err = errorsQueue.GetError() 84 85 totalUploaded = utils.SumIntArray(uploadSummary.SuccessCount) 86 totalUploadAttempted := utils.SumIntArray(uploadSummary.TotalCount) 87 88 log.Debug("Uploaded", strconv.Itoa(totalUploaded), "artifacts.") 89 totalFailed = totalUploadAttempted - totalUploaded 90 if totalFailed > 0 { 91 log.Error("Failed uploading", strconv.Itoa(totalFailed), "artifacts.") 92 } 93 artifactsFileInfo = utils.FlattenFileInfoArray(uploadSummary.FileInfo) 94 return 95 } 96 97 func addProps(oldProps, additionalProps string) string { 98 if len(oldProps) > 0 && !strings.HasSuffix(oldProps, ";") && len(additionalProps) > 0 { 99 oldProps += ";" 100 } 101 return oldProps + additionalProps 102 } 103 104 func addSymlinkProps(artifact clientutils.Artifact, uploadParams UploadParams) (string, error) { 105 artifactProps := "" 106 artifactSymlink := artifact.Symlink 107 if uploadParams.IsSymlink() && len(artifactSymlink) > 0 { 108 sha1Property := "" 109 fileInfo, err := os.Stat(artifact.LocalPath) 110 if err != nil { 111 // If error occurred, but not due to nonexistence of Symlink target -> return empty 112 if !os.IsNotExist(err) { 113 return "", err 114 } 115 // If Symlink target exists -> get SHA1 if isn't a directory 116 } else if !fileInfo.IsDir() { 117 file, err := os.Open(artifact.LocalPath) 118 if err != nil { 119 return "", errorutils.CheckError(err) 120 } 121 defer file.Close() 122 checksumInfo, err := checksum.Calc(file, checksum.SHA1) 123 if err != nil { 124 return "", err 125 } 126 sha1 := checksumInfo[checksum.SHA1] 127 sha1Property = ";" + utils.SYMLINK_SHA1 + "=" + sha1 128 } 129 artifactProps += utils.ARTIFACTORY_SYMLINK + "=" + artifactSymlink + sha1Property 130 } 131 props := uploadParams.GetProps() 132 artifactProps = addProps(props, artifactProps) 133 return artifactProps, nil 134 } 135 136 func collectFilesForUpload(uploadParams UploadParams, producer parallel.Runner, artifactHandlerFunc artifactContext, errorsQueue *clientutils.ErrorsQueue, vcsCache *clientutils.VcsCache) error { 137 if strings.Index(uploadParams.GetTarget(), "/") < 0 { 138 uploadParams.SetTarget(uploadParams.GetTarget() + "/") 139 } 140 uploadParams.SetPattern(clientutils.ReplaceTildeWithUserHome(uploadParams.GetPattern())) 141 // Save parentheses index in pattern, witch have corresponding placeholder. 142 rootPath, err := fspatterns.GetRootPath(uploadParams.GetPattern(), uploadParams.GetTarget(), uploadParams.IsRegexp(), uploadParams.IsSymlink()) 143 if err != nil { 144 return err 145 } 146 147 isDir, err := fileutils.IsDirExists(rootPath, uploadParams.IsSymlink()) 148 if err != nil { 149 return err 150 } 151 152 // If the path is a single file (or a symlink while preserving symlinks) upload it and return 153 if !isDir || (fileutils.IsPathSymlink(rootPath) && uploadParams.IsSymlink()) { 154 artifact, err := fspatterns.GetSingleFileToUpload(rootPath, uploadParams.GetTarget(), uploadParams.IsFlat(), uploadParams.IsSymlink()) 155 if err != nil { 156 return err 157 } 158 props, err := addSymlinkProps(artifact, uploadParams) 159 if err != nil { 160 return err 161 } 162 if uploadParams.IsAddVcsProps() { 163 vcsProps, err := getVcsProps(artifact.LocalPath, vcsCache) 164 if err != nil { 165 return err 166 } 167 uploadParams.BuildProps += vcsProps 168 } 169 uploadData := UploadData{Artifact: artifact, Props: props, BuildProps: uploadParams.BuildProps} 170 task := artifactHandlerFunc(uploadData) 171 producer.AddTaskWithError(task, errorsQueue.AddError) 172 return err 173 } 174 uploadParams.SetPattern(clientutils.PrepareLocalPathForUpload(uploadParams.GetPattern(), uploadParams.IsRegexp())) 175 err = collectPatternMatchingFiles(uploadParams, rootPath, producer, artifactHandlerFunc, errorsQueue, vcsCache) 176 return err 177 } 178 179 func collectPatternMatchingFiles(uploadParams UploadParams, rootPath string, producer parallel.Runner, artifactHandlerFunc artifactContext, errorsQueue *clientutils.ErrorsQueue, vcsCache *clientutils.VcsCache) error { 180 excludePathPattern := fspatterns.PrepareExcludePathPattern(uploadParams) 181 patternRegex, err := regexp.Compile(uploadParams.GetPattern()) 182 if errorutils.CheckError(err) != nil { 183 return err 184 } 185 186 paths, err := fspatterns.GetPaths(rootPath, uploadParams.IsRecursive(), uploadParams.IsIncludeDirs(), uploadParams.IsSymlink()) 187 if err != nil { 188 return err 189 } 190 // Longest paths first 191 sort.Sort(sort.Reverse(sort.StringSlice(paths))) 192 // 'foldersPaths' is a subset of the 'paths' array. foldersPaths is in use only when we need to upload folders with flat=true. 193 // 'foldersPaths' will contain only the directories paths which are in the 'paths' array. 194 var foldersPaths []string 195 for index, path := range paths { 196 matches, isDir, isSymlinkFlow, err := fspatterns.PrepareAndFilterPaths(path, excludePathPattern, uploadParams.IsSymlink(), uploadParams.IsIncludeDirs(), patternRegex) 197 if err != nil { 198 return err 199 } 200 201 if matches != nil && len(matches) > 0 { 202 target := uploadParams.GetTarget() 203 tempPaths := paths 204 tempIndex := index 205 // In case we need to upload directories with flat=true, we want to avoid the creation of unnecessary paths in Artifactory. 206 // To achieve this, we need to take into consideration the directories which had already been uploaded, ignoring all files paths. 207 // When flat=false we take into consideration folder paths which were created implicitly by file upload 208 if uploadParams.IsFlat() && uploadParams.IsIncludeDirs() && isDir { 209 foldersPaths = append(foldersPaths, path) 210 tempPaths = foldersPaths 211 tempIndex = len(foldersPaths) - 1 212 } 213 taskData := &uploadTaskData{target: target, path: path, isDir: isDir, isSymlinkFlow: isSymlinkFlow, 214 paths: tempPaths, groups: matches, index: tempIndex, size: len(matches), uploadParams: uploadParams, 215 producer: producer, artifactHandlerFunc: artifactHandlerFunc, errorsQueue: errorsQueue, 216 } 217 createUploadTask(taskData, vcsCache) 218 } 219 } 220 return nil 221 } 222 223 type uploadTaskData struct { 224 target string 225 path string 226 isDir bool 227 isSymlinkFlow bool 228 paths []string 229 groups []string 230 index int 231 size int 232 uploadParams UploadParams 233 producer parallel.Runner 234 artifactHandlerFunc artifactContext 235 errorsQueue *clientutils.ErrorsQueue 236 } 237 238 func createUploadTask(taskData *uploadTaskData, vcsCache *clientutils.VcsCache) error { 239 for i := 1; i < taskData.size; i++ { 240 group := strings.Replace(taskData.groups[i], "\\", "/", -1) 241 taskData.target = strings.Replace(taskData.target, "{"+strconv.Itoa(i)+"}", group, -1) 242 } 243 var task parallel.TaskFunc 244 245 // Get symlink target (returns empty string if regular file) - Used in upload name / symlinks properties 246 symlinkPath, err := fspatterns.GetFileSymlinkPath(taskData.path) 247 if err != nil { 248 return err 249 } 250 251 // If preserving symlinks or symlink target is empty, use root path name for upload (symlink itself / regular file) 252 if taskData.uploadParams.IsSymlink() || symlinkPath == "" { 253 taskData.target = getUploadTarget(taskData.path, taskData.target, taskData.uploadParams.IsFlat()) 254 } else { 255 taskData.target = getUploadTarget(symlinkPath, taskData.target, taskData.uploadParams.IsFlat()) 256 } 257 258 artifact := clientutils.Artifact{LocalPath: taskData.path, TargetPath: taskData.target, Symlink: symlinkPath} 259 props, e := addSymlinkProps(artifact, taskData.uploadParams) 260 if e != nil { 261 return e 262 } 263 if taskData.uploadParams.IsAddVcsProps() { 264 vcsProps, err := getVcsProps(taskData.path, vcsCache) 265 if err != nil { 266 return err 267 } 268 taskData.uploadParams.BuildProps += vcsProps 269 } 270 uploadData := UploadData{Artifact: artifact, Props: props, BuildProps: taskData.uploadParams.BuildProps} 271 if taskData.isDir && taskData.uploadParams.IsIncludeDirs() && !taskData.isSymlinkFlow { 272 if taskData.path != "." && (taskData.index == 0 || !utils.IsSubPath(taskData.paths, taskData.index, fileutils.GetFileSeparator())) { 273 uploadData.IsDir = true 274 } else { 275 return nil 276 } 277 } 278 task = taskData.artifactHandlerFunc(uploadData) 279 taskData.producer.AddTaskWithError(task, taskData.errorsQueue.AddError) 280 return nil 281 } 282 283 // Construct the target path while taking `flat` flag into account. 284 func getUploadTarget(rootPath, target string, isFlat bool) string { 285 if strings.HasSuffix(target, "/") { 286 if isFlat { 287 fileName, _ := fileutils.GetFileAndDirFromPath(rootPath) 288 target += fileName 289 } else { 290 target += clientutils.TrimPath(rootPath) 291 } 292 } 293 return target 294 } 295 296 func addPropsToTargetPath(targetPath, props, buildProps, debConfig string) (string, error) { 297 propsStr := strings.Join([]string{props, getDebianProps(debConfig)}, ";") 298 properties, err := utils.ParseProperties(propsStr, utils.SplitCommas) 299 if err != nil { 300 return "", err 301 } 302 buildProperties, err := utils.ParseProperties(buildProps, utils.JoinCommas) 303 if err != nil { 304 return "", err 305 } 306 return strings.Join([]string{targetPath, properties.ToEncodedString(), buildProperties.ToEncodedString()}, ";"), nil 307 } 308 309 func prepareUploadData(localPath, baseTargetPath, props, buildProps string, uploadParams UploadParams, logMsgPrefix string) (fileInfo os.FileInfo, targetPath string, err error) { 310 targetPath, err = addPropsToTargetPath(baseTargetPath, props, buildProps, uploadParams.GetDebian()) 311 if errorutils.CheckError(err) != nil { 312 return 313 } 314 log.Info(logMsgPrefix+"Uploading artifact:", localPath) 315 316 fileInfo, err = os.Lstat(localPath) 317 errorutils.CheckError(err) 318 return 319 } 320 321 // Uploads the file in the specified local path to the specified target path. 322 // Returns true if the file was successfully uploaded. 323 func (us *UploadService) uploadFile(localPath, targetPath, pathInArtifactory, props, buildProps string, uploadParams UploadParams, logMsgPrefix string) (utils.FileInfo, bool, error) { 324 fileInfo, targetPathWithProps, err := prepareUploadData(localPath, targetPath, props, buildProps, uploadParams, logMsgPrefix) 325 if err != nil { 326 return utils.FileInfo{}, false, err 327 } 328 329 var checksumDeployed = false 330 var resp *http.Response 331 var details *fileutils.FileDetails 332 var body []byte 333 httpClientsDetails := us.ArtDetails.CreateHttpClientDetails() 334 if errorutils.CheckError(err) != nil { 335 return utils.FileInfo{}, false, err 336 } 337 if uploadParams.IsSymlink() && fileutils.IsFileSymlink(fileInfo) { 338 resp, details, body, err = us.uploadSymlink(targetPathWithProps, logMsgPrefix, httpClientsDetails, uploadParams) 339 } else { 340 resp, details, body, checksumDeployed, err = us.doUpload(localPath, targetPathWithProps, logMsgPrefix, httpClientsDetails, fileInfo, uploadParams) 341 } 342 if err != nil { 343 return utils.FileInfo{}, false, err 344 } 345 logUploadResponse(logMsgPrefix, resp, body, checksumDeployed, us.DryRun) 346 artifact := createBuildArtifactItem(details, localPath, targetPath, pathInArtifactory) 347 return artifact, us.DryRun || checksumDeployed || resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK, nil 348 } 349 350 func (us *UploadService) uploadSymlink(targetPath, logMsgPrefix string, httpClientsDetails httputils.HttpClientDetails, uploadParams UploadParams) (resp *http.Response, details *fileutils.FileDetails, body []byte, err error) { 351 details, err = fspatterns.CreateSymlinkFileDetails() 352 if err != nil { 353 return 354 } 355 resp, body, err = utils.UploadFile("", targetPath, logMsgPrefix, &us.ArtDetails, details, httpClientsDetails, us.client, uploadParams.GetRetries(), nil) 356 return 357 } 358 359 func (us *UploadService) doUpload(localPath, targetPath, logMsgPrefix string, httpClientsDetails httputils.HttpClientDetails, fileInfo os.FileInfo, uploadParams UploadParams) (*http.Response, *fileutils.FileDetails, []byte, bool, error) { 360 var details *fileutils.FileDetails 361 var checksumDeployed bool 362 var resp *http.Response 363 var body []byte 364 var err error 365 addExplodeHeader(&httpClientsDetails, uploadParams.IsExplodeArchive()) 366 if fileInfo.Size() >= uploadParams.MinChecksumDeploy && !uploadParams.IsExplodeArchive() { 367 resp, details, body, err = us.tryChecksumDeploy(localPath, targetPath, httpClientsDetails, us.client) 368 if err != nil { 369 return resp, details, body, checksumDeployed, err 370 } 371 checksumDeployed = !us.DryRun && (resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK) 372 } 373 if !us.DryRun && !checksumDeployed { 374 var body []byte 375 resp, body, err = utils.UploadFile(localPath, targetPath, logMsgPrefix, &us.ArtDetails, details, 376 httpClientsDetails, us.client, uploadParams.Retries, us.Progress) 377 if err != nil { 378 return resp, details, body, checksumDeployed, err 379 } 380 } 381 if details == nil { 382 details, err = fileutils.GetFileDetails(localPath) 383 } 384 return resp, details, body, checksumDeployed, err 385 } 386 387 func logUploadResponse(logMsgPrefix string, resp *http.Response, body []byte, checksumDeployed, isDryRun bool) { 388 if resp != nil && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { 389 log.Error(logMsgPrefix + "Artifactory response: " + resp.Status + "\n" + clientutils.IndentJson(body)) 390 return 391 } 392 if !isDryRun { 393 var strChecksumDeployed string 394 if checksumDeployed { 395 strChecksumDeployed = " (Checksum deploy)" 396 } else { 397 strChecksumDeployed = "" 398 } 399 log.Debug(logMsgPrefix, "Artifactory response:", resp.Status, strChecksumDeployed) 400 } 401 } 402 403 func createBuildArtifactItem(details *fileutils.FileDetails, localPath, targetPath, pathInArtifactory string) utils.FileInfo { 404 return utils.FileInfo{ 405 LocalPath: localPath, 406 ArtifactoryPath: targetPath, 407 InternalArtifactoryPath: pathInArtifactory, 408 FileHashes: &utils.FileHashes{ 409 Sha256: details.Checksum.Sha256, 410 Sha1: details.Checksum.Sha1, 411 Md5: details.Checksum.Md5, 412 }, 413 } 414 } 415 416 func addExplodeHeader(httpClientsDetails *httputils.HttpClientDetails, isExplode bool) { 417 if isExplode { 418 utils.AddHeader("X-Explode-Archive", "true", &httpClientsDetails.Headers) 419 } 420 } 421 422 func (us *UploadService) tryChecksumDeploy(filePath, targetPath string, httpClientsDetails httputils.HttpClientDetails, 423 client *rthttpclient.ArtifactoryHttpClient) (resp *http.Response, details *fileutils.FileDetails, body []byte, err error) { 424 if us.DryRun { 425 return 426 } 427 details, err = fileutils.GetFileDetails(filePath) 428 if err != nil { 429 return 430 } 431 432 requestClientDetails := httpClientsDetails.Clone() 433 utils.AddHeader("X-Checksum-Deploy", "true", &requestClientDetails.Headers) 434 utils.AddChecksumHeaders(requestClientDetails.Headers, details) 435 utils.AddAuthHeaders(requestClientDetails.Headers, us.ArtDetails) 436 437 resp, body, err = client.SendPut(targetPath, nil, requestClientDetails) 438 return 439 } 440 441 func getDebianProps(debianPropsStr string) string { 442 if debianPropsStr == "" { 443 return "" 444 } 445 result := "" 446 debProps := clientutils.SplitWithEscape(debianPropsStr, '/') 447 for k, v := range []string{"deb.distribution", "deb.component", "deb.architecture"} { 448 debProp := strings.Join([]string{v, debProps[k]}, "=") 449 result = strings.Join([]string{result, debProp}, ";") 450 } 451 return result 452 } 453 454 type UploadParams struct { 455 *utils.ArtifactoryCommonParams 456 Deb string 457 BuildProps string 458 Symlink bool 459 ExplodeArchive bool 460 Flat bool 461 AddVcsProps bool 462 Retries int 463 MinChecksumDeploy int64 464 } 465 466 func (up *UploadParams) IsFlat() bool { 467 return up.Flat 468 } 469 470 func (up *UploadParams) IsSymlink() bool { 471 return up.Symlink 472 } 473 474 func (up *UploadParams) IsAddVcsProps() bool { 475 return up.AddVcsProps 476 } 477 478 func (up *UploadParams) IsExplodeArchive() bool { 479 return up.ExplodeArchive 480 } 481 482 func (up *UploadParams) GetDebian() string { 483 return up.Deb 484 } 485 486 func (up *UploadParams) GetRetries() int { 487 return up.Retries 488 } 489 490 type UploadData struct { 491 Artifact clientutils.Artifact 492 Props string 493 BuildProps string 494 IsDir bool 495 } 496 497 type artifactContext func(UploadData) parallel.TaskFunc 498 499 func (us *UploadService) createArtifactHandlerFunc(uploadResult *utils.UploadResult, uploadParams UploadParams) artifactContext { 500 return func(artifact UploadData) parallel.TaskFunc { 501 return func(threadId int) (e error) { 502 if artifact.IsDir { 503 us.createFolderInArtifactory(artifact) 504 return 505 } 506 var uploaded bool 507 var target string 508 var artifactFileInfo utils.FileInfo 509 uploadResult.TotalCount[threadId]++ 510 logMsgPrefix := clientutils.GetLogMsgPrefix(threadId, us.DryRun) 511 target, e = utils.BuildArtifactoryUrl(us.ArtDetails.GetUrl(), artifact.Artifact.TargetPath, make(map[string]string)) 512 if e != nil { 513 return 514 } 515 artifactFileInfo, uploaded, e = us.uploadFile(artifact.Artifact.LocalPath, target, artifact.Artifact.TargetPath, artifact.Props, artifact.BuildProps, uploadParams, logMsgPrefix) 516 if e != nil { 517 return 518 } 519 if uploaded { 520 uploadResult.SuccessCount[threadId]++ 521 uploadResult.FileInfo[threadId] = append(uploadResult.FileInfo[threadId], artifactFileInfo) 522 } 523 return 524 } 525 } 526 } 527 528 func (us *UploadService) createFolderInArtifactory(artifact UploadData) error { 529 url, err := utils.BuildArtifactoryUrl(us.ArtDetails.GetUrl(), artifact.Artifact.TargetPath, make(map[string]string)) 530 url = clientutils.AddTrailingSlashIfNeeded(url) 531 if err != nil { 532 return err 533 } 534 content := make([]byte, 0) 535 httpClientsDetails := us.ArtDetails.CreateHttpClientDetails() 536 resp, body, err := us.client.SendPut(url, content, &httpClientsDetails) 537 if err != nil { 538 log.Debug(resp) 539 return err 540 } 541 logUploadResponse("Uploaded directory:", resp, body, false, us.DryRun) 542 return err 543 } 544 545 func NewUploadParams() UploadParams { 546 return UploadParams{ArtifactoryCommonParams: &utils.ArtifactoryCommonParams{}, MinChecksumDeploy: 10240} 547 } 548 549 func getVcsProps(path string, vcsCache *clientutils.VcsCache) (string, error) { 550 path, err := filepath.Abs(path) 551 if err != nil { 552 return "", errorutils.CheckError(err) 553 } 554 props := "" 555 revision, url, err := vcsCache.GetVcsDetails(filepath.Dir(path)) 556 if err != nil { 557 return "", errorutils.CheckError(err) 558 } 559 if revision != "" { 560 props += ";vcs.revision=" + revision 561 } 562 if url != "" { 563 props += ";vcs.url=" + url 564 } 565 return props, nil 566 }