github.com/cobalt77/jfrog-client-go@v0.14.5/artifactory/services/download.go (about) 1 package services 2 3 import ( 4 "errors" 5 "net/http" 6 "os" 7 "path" 8 "path/filepath" 9 "sort" 10 11 rthttpclient "github.com/cobalt77/jfrog-client-go/artifactory/httpclient" 12 "github.com/cobalt77/jfrog-client-go/artifactory/services/utils" 13 "github.com/cobalt77/jfrog-client-go/auth" 14 "github.com/cobalt77/jfrog-client-go/httpclient" 15 clientutils "github.com/cobalt77/jfrog-client-go/utils" 16 "github.com/cobalt77/jfrog-client-go/utils/errorutils" 17 clientio "github.com/cobalt77/jfrog-client-go/utils/io" 18 "github.com/cobalt77/jfrog-client-go/utils/io/content" 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/log" 22 "github.com/jfrog/gofrog/parallel" 23 "github.com/mholt/archiver/v3" 24 ) 25 26 type DownloadService struct { 27 client *rthttpclient.ArtifactoryHttpClient 28 Progress clientio.Progress 29 ArtDetails auth.ServiceDetails 30 DryRun bool 31 Threads int 32 ResultWriter *content.ContentWriter 33 } 34 35 func NewDownloadService(client *rthttpclient.ArtifactoryHttpClient) *DownloadService { 36 return &DownloadService{client: client} 37 } 38 39 func (ds *DownloadService) GetArtifactoryDetails() auth.ServiceDetails { 40 return ds.ArtDetails 41 } 42 43 func (ds *DownloadService) SetArtifactoryDetails(rt auth.ServiceDetails) { 44 ds.ArtDetails = rt 45 } 46 47 func (ds *DownloadService) IsDryRun() bool { 48 return ds.DryRun 49 } 50 51 func (ds *DownloadService) GetJfrogHttpClient() (*rthttpclient.ArtifactoryHttpClient, error) { 52 return ds.client, nil 53 } 54 55 func (ds *DownloadService) GetThreads() int { 56 return ds.Threads 57 } 58 59 func (ds *DownloadService) SetThreads(threads int) { 60 ds.Threads = threads 61 } 62 63 func (ds *DownloadService) SetServiceDetails(artDetails auth.ServiceDetails) { 64 ds.ArtDetails = artDetails 65 } 66 67 func (ds *DownloadService) SetDryRun(isDryRun bool) { 68 ds.DryRun = isDryRun 69 } 70 71 func (ds *DownloadService) DownloadFiles(downloadParams ...DownloadParams) (int, int, error) { 72 producerConsumer := parallel.NewBounedRunner(ds.GetThreads(), false) 73 errorsQueue := clientutils.NewErrorsQueue(1) 74 expectedChan := make(chan int, 1) 75 successCounters := make([]int, ds.GetThreads()) 76 ds.prepareTasks(producerConsumer, expectedChan, successCounters, errorsQueue, downloadParams...) 77 78 err := ds.performTasks(producerConsumer, errorsQueue) 79 totalSuccess := 0 80 for _, v := range successCounters { 81 totalSuccess += v 82 } 83 return totalSuccess, <-expectedChan, err 84 } 85 86 func (ds *DownloadService) prepareTasks(producer parallel.Runner, expectedChan chan int, successCounters []int, errorsQueue *clientutils.ErrorsQueue, downloadParamsSlice ...DownloadParams) { 87 go func() { 88 defer producer.Done() 89 defer close(expectedChan) 90 totalTasks := 0 91 // Iterate over file-spec groups and produce download tasks. 92 // When encountering an error, log and move to next group. 93 for _, downloadParams := range downloadParamsSlice { 94 var err error 95 var reader *content.ContentReader 96 // Create handler function for the current group. 97 fileHandlerFunc := ds.createFileHandlerFunc(downloadParams, successCounters) 98 // Search items. 99 log.Info("Searching items to download...") 100 switch downloadParams.GetSpecType() { 101 case utils.WILDCARD: 102 reader, err = ds.collectFilesUsingWildcardPattern(downloadParams) 103 case utils.BUILD: 104 reader, err = utils.SearchBySpecWithBuild(downloadParams.GetFile(), ds) 105 case utils.AQL: 106 reader, err = utils.SearchBySpecWithAql(downloadParams.GetFile(), ds, utils.SYMLINK) 107 } 108 // Check for search errors. 109 if err != nil { 110 log.Error(err) 111 errorsQueue.AddError(err) 112 continue 113 } 114 // Produce download tasks for the download consumers. 115 totalTasks += produceTasks(reader, downloadParams, producer, fileHandlerFunc, errorsQueue) 116 reader.Close() 117 } 118 expectedChan <- totalTasks 119 }() 120 } 121 122 func (ds *DownloadService) collectFilesUsingWildcardPattern(downloadParams DownloadParams) (*content.ContentReader, error) { 123 return utils.SearchBySpecWithPattern(downloadParams.GetFile(), ds, utils.SYMLINK) 124 } 125 126 func produceTasks(reader *content.ContentReader, downloadParams DownloadParams, producer parallel.Runner, fileHandler fileHandlerFunc, errorsQueue *clientutils.ErrorsQueue) int { 127 flat := downloadParams.IsFlat() 128 // Collect all folders path which might be needed to create. 129 // key = folder path, value = the necessary data for producing create folder task. 130 directoriesData := make(map[string]DownloadData) 131 // Store all the paths which was created implicitly due to file upload. 132 alreadyCreatedDirs := make(map[string]bool) 133 // Store all the keys of directoriesData as an array. 134 var directoriesDataKeys []string 135 // Task counter 136 var tasksCount int 137 for resultItem := new(utils.ResultItem); reader.NextRecord(resultItem) == nil; resultItem = new(utils.ResultItem) { 138 tempData := DownloadData{ 139 Dependency: *resultItem, 140 DownloadPath: downloadParams.GetPattern(), 141 Target: downloadParams.GetTarget(), 142 Flat: flat, 143 } 144 if resultItem.Type != "folder" { 145 // Add a task. A task is a function of type TaskFunc which later on will be executed by other go routine, the communication is done using channels. 146 // The second argument is an error handling func in case the taskFunc return an error. 147 tasksCount++ 148 producer.AddTaskWithError(fileHandler(tempData), errorsQueue.AddError) 149 // We don't want to create directories which are created explicitly by download files when ArtifactoryCommonParams.IncludeDirs is used. 150 alreadyCreatedDirs[resultItem.Path] = true 151 } else { 152 directoriesData, directoriesDataKeys = collectDirPathsToCreate(*resultItem, directoriesData, tempData, directoriesDataKeys) 153 } 154 } 155 if err := reader.GetError(); err != nil { 156 errorsQueue.AddError(errorutils.CheckError(err)) 157 return tasksCount 158 } 159 reader.Reset() 160 addCreateDirsTasks(directoriesDataKeys, alreadyCreatedDirs, producer, fileHandler, directoriesData, errorsQueue, flat) 161 return tasksCount 162 } 163 164 // Extract for the aqlResultItem the directory path, store the path the directoriesDataKeys and in the directoriesData map. 165 // In addition directoriesData holds the correlate DownloadData for each key, later on this DownloadData will be used to create a create dir tasks if needed. 166 // This function append the new data to directoriesDataKeys and to directoriesData and return the new map and the new []string 167 // We are storing all the keys of directoriesData in additional array(directoriesDataKeys) so we could sort the keys and access the maps in the sorted order. 168 func collectDirPathsToCreate(aqlResultItem utils.ResultItem, directoriesData map[string]DownloadData, tempData DownloadData, directoriesDataKeys []string) (map[string]DownloadData, []string) { 169 key := aqlResultItem.Name 170 if aqlResultItem.Path != "." { 171 key = path.Join(aqlResultItem.Path, aqlResultItem.Name) 172 } 173 directoriesData[key] = tempData 174 directoriesDataKeys = append(directoriesDataKeys, key) 175 return directoriesData, directoriesDataKeys 176 } 177 178 func addCreateDirsTasks(directoriesDataKeys []string, alreadyCreatedDirs map[string]bool, producer parallel.Runner, fileHandler fileHandlerFunc, directoriesData map[string]DownloadData, errorsQueue *clientutils.ErrorsQueue, isFlat bool) { 179 // Longest path first 180 // We are going to create the longest path first by doing so all sub paths of the longest path will be created implicitly. 181 sort.Sort(sort.Reverse(sort.StringSlice(directoriesDataKeys))) 182 for index, v := range directoriesDataKeys { 183 // In order to avoid duplication we need to check the path wasn't already created by the previous action. 184 if v != "." && // For some files the returned path can be the root path, ".", in that case we doing need to create any directory. 185 (index == 0 || !utils.IsSubPath(directoriesDataKeys, index, "/")) { // directoriesDataKeys store all the path which might needed to be created, that's include duplicated paths. 186 // By sorting the directoriesDataKeys we can assure that the longest path was created and therefore no need to create all it's sub paths. 187 188 // Some directories were created due to file download when we aren't in flat download flow. 189 if isFlat { 190 producer.AddTaskWithError(fileHandler(directoriesData[v]), errorsQueue.AddError) 191 } else if !alreadyCreatedDirs[v] { 192 producer.AddTaskWithError(fileHandler(directoriesData[v]), errorsQueue.AddError) 193 } 194 } 195 } 196 return 197 } 198 199 func (ds *DownloadService) performTasks(consumer parallel.Runner, errorsQueue *clientutils.ErrorsQueue) error { 200 // Blocked until finish consuming 201 consumer.Run() 202 if ds.ResultWriter != nil { 203 err := ds.ResultWriter.Close() 204 if err != nil { 205 return err 206 } 207 } 208 return errorsQueue.GetError() 209 } 210 211 func createDependencyFileInfo(resultItem utils.ResultItem, localPath, localFileName string) utils.FileInfo { 212 fileInfo := utils.FileInfo{ 213 ArtifactoryPath: resultItem.GetItemRelativePath(), 214 FileHashes: &utils.FileHashes{ 215 Sha1: resultItem.Actual_Sha1, 216 Md5: resultItem.Actual_Md5, 217 }, 218 } 219 fileInfo.LocalPath = filepath.Join(localPath, localFileName) 220 return fileInfo 221 } 222 223 func createDownloadFileDetails(downloadPath, localPath, localFileName string, downloadData DownloadData) (details *httpclient.DownloadFileDetails) { 224 details = &httpclient.DownloadFileDetails{ 225 FileName: downloadData.Dependency.Name, 226 DownloadPath: downloadPath, 227 RelativePath: downloadData.Dependency.GetItemRelativePath(), 228 LocalPath: localPath, 229 LocalFileName: localFileName, 230 Size: downloadData.Dependency.Size, 231 ExpectedSha1: downloadData.Dependency.Actual_Sha1} 232 return 233 } 234 235 func (ds *DownloadService) downloadFile(downloadFileDetails *httpclient.DownloadFileDetails, logMsgPrefix string, downloadParams DownloadParams) error { 236 httpClientsDetails := ds.ArtDetails.CreateHttpClientDetails() 237 bulkDownload := downloadParams.SplitCount == 0 || downloadParams.MinSplitSize < 0 || downloadParams.MinSplitSize*1000 > downloadFileDetails.Size 238 if !bulkDownload { 239 acceptRange, err := ds.isFileAcceptRange(downloadFileDetails) 240 if err != nil { 241 return err 242 } 243 bulkDownload = !acceptRange 244 } 245 if bulkDownload { 246 var resp *http.Response 247 resp, err := ds.client.DownloadFileWithProgress(downloadFileDetails, logMsgPrefix, &httpClientsDetails, 248 downloadParams.GetRetries(), downloadParams.IsExplode(), ds.Progress) 249 if err != nil { 250 return err 251 } 252 log.Debug(logMsgPrefix, "Artifactory response:", resp.Status) 253 return errorutils.CheckResponseStatus(resp, http.StatusOK) 254 } 255 256 concurrentDownloadFlags := httpclient.ConcurrentDownloadFlags{ 257 FileName: downloadFileDetails.FileName, 258 DownloadPath: downloadFileDetails.DownloadPath, 259 RelativePath: downloadFileDetails.RelativePath, 260 LocalFileName: downloadFileDetails.LocalFileName, 261 LocalPath: downloadFileDetails.LocalPath, 262 ExpectedSha1: downloadFileDetails.ExpectedSha1, 263 FileSize: downloadFileDetails.Size, 264 SplitCount: downloadParams.SplitCount, 265 Explode: downloadParams.IsExplode(), 266 Retries: downloadParams.GetRetries()} 267 268 resp, err := ds.client.DownloadFileConcurrently(concurrentDownloadFlags, logMsgPrefix, &httpClientsDetails, ds.Progress) 269 if err != nil { 270 return err 271 } 272 return errorutils.CheckResponseStatus(resp, http.StatusPartialContent) 273 } 274 275 func (ds *DownloadService) isFileAcceptRange(downloadFileDetails *httpclient.DownloadFileDetails) (bool, error) { 276 httpClientsDetails := ds.ArtDetails.CreateHttpClientDetails() 277 isAcceptRange, resp, err := ds.client.IsAcceptRanges(downloadFileDetails.DownloadPath, &httpClientsDetails) 278 if err != nil { 279 return false, err 280 } 281 err = errorutils.CheckResponseStatus(resp, http.StatusOK) 282 if err != nil { 283 return false, err 284 } 285 return isAcceptRange, err 286 } 287 288 func removeIfSymlink(localSymlinkPath string) error { 289 if fileutils.IsPathSymlink(localSymlinkPath) { 290 if err := os.Remove(localSymlinkPath); errorutils.CheckError(err) != nil { 291 return err 292 } 293 } 294 return nil 295 } 296 297 func createLocalSymlink(localPath, localFileName, symlinkArtifact string, symlinkChecksum bool, symlinkContentChecksum string, logMsgPrefix string) error { 298 if symlinkChecksum && symlinkContentChecksum != "" { 299 if !fileutils.IsPathExists(symlinkArtifact, false) { 300 return errorutils.CheckError(errors.New("Symlink validation failed, target doesn't exist: " + symlinkArtifact)) 301 } 302 file, err := os.Open(symlinkArtifact) 303 if err = errorutils.CheckError(err); err != nil { 304 return err 305 } 306 defer file.Close() 307 checksumInfo, err := checksum.Calc(file, checksum.SHA1) 308 if err != nil { 309 return err 310 } 311 sha1 := checksumInfo[checksum.SHA1] 312 if sha1 != symlinkContentChecksum { 313 return errorutils.CheckError(errors.New("Symlink validation failed for target: " + symlinkArtifact)) 314 } 315 } 316 localSymlinkPath := filepath.Join(localPath, localFileName) 317 isFileExists, err := fileutils.IsFileExists(localSymlinkPath, false) 318 if err != nil { 319 return err 320 } 321 // We can't create symlink in case a file with the same name already exist, we must remove the file before creating the symlink 322 if isFileExists { 323 if err := os.Remove(localSymlinkPath); err != nil { 324 return err 325 } 326 } 327 // Need to prepare the directories hierarchy 328 _, err = fileutils.CreateFilePath(localPath, localFileName) 329 if err != nil { 330 return err 331 } 332 err = os.Symlink(symlinkArtifact, localSymlinkPath) 333 if errorutils.CheckError(err) != nil { 334 return err 335 } 336 log.Debug(logMsgPrefix, "Creating symlink file.") 337 return nil 338 } 339 340 func getArtifactPropertyByKey(properties []utils.Property, key string) string { 341 for _, v := range properties { 342 if v.Key == key { 343 return v.Value 344 } 345 } 346 return "" 347 } 348 349 func getArtifactSymlinkPath(properties []utils.Property) string { 350 return getArtifactPropertyByKey(properties, utils.ARTIFACTORY_SYMLINK) 351 } 352 353 func getArtifactSymlinkChecksum(properties []utils.Property) string { 354 return getArtifactPropertyByKey(properties, utils.SYMLINK_SHA1) 355 } 356 357 type fileHandlerFunc func(DownloadData) parallel.TaskFunc 358 359 func (ds *DownloadService) createFileHandlerFunc(downloadParams DownloadParams, successCounters []int) fileHandlerFunc { 360 return func(downloadData DownloadData) parallel.TaskFunc { 361 return func(threadId int) error { 362 logMsgPrefix := clientutils.GetLogMsgPrefix(threadId, ds.DryRun) 363 downloadPath, e := utils.BuildArtifactoryUrl(ds.ArtDetails.GetUrl(), downloadData.Dependency.GetItemRelativePath(), make(map[string]string)) 364 if e != nil { 365 return e 366 } 367 log.Info(logMsgPrefix+"Downloading", downloadData.Dependency.GetItemRelativePath()) 368 if ds.DryRun { 369 return nil 370 } 371 target, e := clientutils.BuildTargetPath(downloadData.DownloadPath, downloadData.Dependency.GetItemRelativePath(), downloadData.Target, true) 372 if e != nil { 373 return e 374 } 375 localPath, localFileName := fileutils.GetLocalPathAndFile(downloadData.Dependency.Name, downloadData.Dependency.Path, target, downloadData.Flat) 376 if downloadData.Dependency.Type == "folder" { 377 return createDir(localPath, localFileName, logMsgPrefix) 378 } 379 e = removeIfSymlink(filepath.Join(localPath, localFileName)) 380 if e != nil { 381 return e 382 } 383 if downloadParams.IsSymlink() { 384 if isSymlink, e := createSymlinkIfNeeded(localPath, localFileName, logMsgPrefix, downloadData, successCounters, ds.ResultWriter, threadId, downloadParams); isSymlink { 385 return e 386 } 387 } 388 dependency := createDependencyFileInfo(downloadData.Dependency, localPath, localFileName) 389 e = ds.downloadFileIfNeeded(downloadPath, localPath, localFileName, logMsgPrefix, downloadData, downloadParams) 390 if e != nil { 391 log.Error(logMsgPrefix, "Received an error: "+e.Error()) 392 return e 393 } 394 successCounters[threadId]++ 395 if ds.ResultWriter != nil { 396 ds.ResultWriter.Write(dependency) 397 } 398 return nil 399 } 400 } 401 } 402 403 func (ds *DownloadService) downloadFileIfNeeded(downloadPath, localPath, localFileName, logMsgPrefix string, downloadData DownloadData, downloadParams DownloadParams) error { 404 isEqual, e := fileutils.IsEqualToLocalFile(filepath.Join(localPath, localFileName), downloadData.Dependency.Actual_Md5, downloadData.Dependency.Actual_Sha1) 405 if e != nil { 406 return e 407 } 408 if isEqual { 409 log.Debug(logMsgPrefix, "File already exists locally.") 410 if downloadParams.IsExplode() { 411 e = explodeLocalFile(localPath, localFileName) 412 } 413 return e 414 } 415 downloadFileDetails := createDownloadFileDetails(downloadPath, localPath, localFileName, downloadData) 416 return ds.downloadFile(downloadFileDetails, logMsgPrefix, downloadParams) 417 } 418 419 func explodeLocalFile(localPath, localFileName string) (err error) { 420 log.Info("Extracting archive:", localFileName, "to", localPath) 421 arch, err := archiver.ByExtension(localFileName) 422 absolutePath := filepath.Join(localPath, localFileName) 423 424 // The file is indeed an archive 425 if err == nil { 426 err := arch.(archiver.Unarchiver).Unarchive(absolutePath, localPath) 427 if err != nil { 428 return errorutils.CheckError(err) 429 } 430 // If the file was extracted successfully, remove it from the file system 431 err = os.Remove(absolutePath) 432 } 433 434 return errorutils.CheckError(err) 435 } 436 437 func createDir(localPath, localFileName, logMsgPrefix string) error { 438 folderPath := filepath.Join(localPath, localFileName) 439 e := fileutils.CreateDirIfNotExist(folderPath) 440 if e != nil { 441 return e 442 } 443 log.Info(logMsgPrefix + "Creating folder: " + folderPath) 444 return nil 445 } 446 447 func createSymlinkIfNeeded(localPath, localFileName, logMsgPrefix string, downloadData DownloadData, successCounters []int, responseWriter *content.ContentWriter, threadId int, downloadParams DownloadParams) (bool, error) { 448 symlinkArtifact := getArtifactSymlinkPath(downloadData.Dependency.Properties) 449 isSymlink := len(symlinkArtifact) > 0 450 if isSymlink { 451 symlinkChecksum := getArtifactSymlinkChecksum(downloadData.Dependency.Properties) 452 if e := createLocalSymlink(localPath, localFileName, symlinkArtifact, downloadParams.ValidateSymlinks(), symlinkChecksum, logMsgPrefix); e != nil { 453 return isSymlink, e 454 } 455 dependency := createDependencyFileInfo(downloadData.Dependency, localPath, localFileName) 456 successCounters[threadId]++ 457 if responseWriter != nil { 458 responseWriter.Write(dependency) 459 } 460 return isSymlink, nil 461 } 462 return isSymlink, nil 463 } 464 465 type DownloadData struct { 466 Dependency utils.ResultItem 467 DownloadPath string 468 Target string 469 Flat bool 470 } 471 472 type DownloadParams struct { 473 *utils.ArtifactoryCommonParams 474 Symlink bool 475 ValidateSymlink bool 476 Flat bool 477 Explode bool 478 MinSplitSize int64 479 SplitCount int 480 Retries int 481 } 482 483 func (ds *DownloadParams) IsFlat() bool { 484 return ds.Flat 485 } 486 487 func (ds *DownloadParams) IsExplode() bool { 488 return ds.Explode 489 } 490 491 func (ds *DownloadParams) GetFile() *utils.ArtifactoryCommonParams { 492 return ds.ArtifactoryCommonParams 493 } 494 495 func (ds *DownloadParams) IsSymlink() bool { 496 return ds.Symlink 497 } 498 499 func (ds *DownloadParams) ValidateSymlinks() bool { 500 return ds.ValidateSymlink 501 } 502 503 func (ds *DownloadParams) GetRetries() int { 504 return ds.Retries 505 } 506 507 func NewDownloadParams() DownloadParams { 508 return DownloadParams{ArtifactoryCommonParams: &utils.ArtifactoryCommonParams{}, MinSplitSize: 5120, SplitCount: 3, Retries: 3} 509 }