github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/generic/download.go (about) 1 package generic 2 3 import ( 4 "errors" 5 "os" 6 "path/filepath" 7 "strconv" 8 "strings" 9 10 buildinfo "github.com/jfrog/build-info-go/entities" 11 gofrog "github.com/jfrog/gofrog/io" 12 "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" 13 "github.com/jfrog/jfrog-cli-core/v2/common/build" 14 "github.com/jfrog/jfrog-cli-core/v2/common/spec" 15 "github.com/jfrog/jfrog-client-go/artifactory/services" 16 serviceutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" 17 clientutils "github.com/jfrog/jfrog-client-go/utils" 18 "github.com/jfrog/jfrog-client-go/utils/errorutils" 19 ioUtils "github.com/jfrog/jfrog-client-go/utils/io" 20 "github.com/jfrog/jfrog-client-go/utils/io/content" 21 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 22 "github.com/jfrog/jfrog-client-go/utils/log" 23 ) 24 25 type DownloadCommand struct { 26 buildConfiguration *build.BuildConfiguration 27 GenericCommand 28 configuration *utils.DownloadConfiguration 29 progress ioUtils.ProgressMgr 30 } 31 32 func NewDownloadCommand() *DownloadCommand { 33 return &DownloadCommand{GenericCommand: *NewGenericCommand()} 34 } 35 36 func (dc *DownloadCommand) SetBuildConfiguration(buildConfiguration *build.BuildConfiguration) *DownloadCommand { 37 dc.buildConfiguration = buildConfiguration 38 return dc 39 } 40 41 func (dc *DownloadCommand) Configuration() *utils.DownloadConfiguration { 42 return dc.configuration 43 } 44 45 func (dc *DownloadCommand) SetConfiguration(configuration *utils.DownloadConfiguration) *DownloadCommand { 46 dc.configuration = configuration 47 return dc 48 } 49 50 func (dc *DownloadCommand) SetProgress(progress ioUtils.ProgressMgr) { 51 dc.progress = progress 52 } 53 54 func (dc *DownloadCommand) ShouldPrompt() bool { 55 return !dc.DryRun() && dc.SyncDeletesPath() != "" && !dc.Quiet() 56 } 57 58 func (dc *DownloadCommand) CommandName() string { 59 return "rt_download" 60 } 61 62 func (dc *DownloadCommand) Run() error { 63 return dc.download() 64 } 65 66 func (dc *DownloadCommand) download() (err error) { 67 // Init progress bar if needed 68 if dc.progress != nil { 69 dc.progress.InitProgressReaders() 70 } 71 // Create Service Manager: 72 servicesManager, err := utils.CreateDownloadServiceManager(dc.serverDetails, dc.configuration.Threads, dc.retries, dc.retryWaitTimeMilliSecs, dc.DryRun(), dc.progress) 73 if err != nil { 74 return err 75 } 76 77 // Build Info Collection: 78 toCollect, err := dc.buildConfiguration.IsCollectBuildInfo() 79 if err != nil { 80 return err 81 } 82 if toCollect && !dc.DryRun() { 83 var buildName, buildNumber string 84 buildName, err = dc.buildConfiguration.GetBuildName() 85 if err != nil { 86 return err 87 } 88 buildNumber, err = dc.buildConfiguration.GetBuildNumber() 89 if err != nil { 90 return err 91 } 92 if err = build.SaveBuildGeneralDetails(buildName, buildNumber, dc.buildConfiguration.GetProject()); err != nil { 93 return err 94 } 95 } 96 97 var errorOccurred = false 98 var downloadParamsArray []services.DownloadParams 99 // Create DownloadParams for all File-Spec groups. 100 var downParams services.DownloadParams 101 for i := 0; i < len(dc.Spec().Files); i++ { 102 downParams, err = getDownloadParams(dc.Spec().Get(i), dc.configuration) 103 if err != nil { 104 errorOccurred = true 105 log.Error(err) 106 continue 107 } 108 downloadParamsArray = append(downloadParamsArray, downParams) 109 } 110 // Perform download. 111 // In case of build-info collection/sync-deletes operation/a detailed summary is required, we use the download service which provides results file reader, 112 // otherwise we use the download service which provides only general counters. 113 var totalDownloaded, totalFailed int 114 var summary *serviceutils.OperationSummary 115 if toCollect || dc.SyncDeletesPath() != "" || dc.DetailedSummary() { 116 summary, err = servicesManager.DownloadFilesWithSummary(downloadParamsArray...) 117 if err != nil { 118 errorOccurred = true 119 log.Error(err) 120 } 121 if summary != nil { 122 defer gofrog.Close(summary.ArtifactsDetailsReader, &err) 123 // If 'detailed summary' was requested, then the reader should not be closed here. 124 // It will be closed after it will be used to generate the summary. 125 if dc.DetailedSummary() { 126 dc.result.SetReader(summary.TransferDetailsReader) 127 } else { 128 defer gofrog.Close(summary.TransferDetailsReader, &err) 129 } 130 totalDownloaded = summary.TotalSucceeded 131 totalFailed = summary.TotalFailed 132 } 133 } else { 134 totalDownloaded, totalFailed, err = servicesManager.DownloadFiles(downloadParamsArray...) 135 if err != nil { 136 errorOccurred = true 137 log.Error(err) 138 } 139 } 140 dc.result.SetSuccessCount(totalDownloaded) 141 dc.result.SetFailCount(totalFailed) 142 // Check for errors. 143 if errorOccurred { 144 return errors.New("download finished with errors, please review the logs") 145 } 146 if dc.DryRun() { 147 dc.result.SetSuccessCount(totalDownloaded) 148 dc.result.SetFailCount(0) 149 return err 150 } else if dc.SyncDeletesPath() != "" { 151 var absSyncDeletesPath string 152 absSyncDeletesPath, err = filepath.Abs(dc.SyncDeletesPath()) 153 if err != nil { 154 return errorutils.CheckError(err) 155 } 156 if _, err = os.Stat(absSyncDeletesPath); err == nil { 157 // Unmarshal the local paths of the downloaded files from the results file reader 158 var tmpRoot string 159 tmpRoot, err = createDownloadResultEmptyTmpReflection(summary.TransferDetailsReader) 160 defer func() { 161 err = errors.Join(err, fileutils.RemoveTempDir(tmpRoot)) 162 }() 163 if err != nil { 164 return err 165 } 166 walkFn := createSyncDeletesWalkFunction(tmpRoot) 167 err = gofrog.Walk(dc.SyncDeletesPath(), walkFn, false) 168 if err != nil { 169 return errorutils.CheckError(err) 170 } 171 } else if os.IsNotExist(err) { 172 log.Info("Sync-deletes path", absSyncDeletesPath, "does not exists.") 173 } 174 } 175 log.Debug("Downloaded", strconv.Itoa(totalDownloaded), "artifacts.") 176 177 // Build Info 178 if toCollect { 179 var buildName, buildNumber string 180 buildName, err = dc.buildConfiguration.GetBuildName() 181 if err != nil { 182 return err 183 } 184 buildNumber, err = dc.buildConfiguration.GetBuildNumber() 185 if err != nil { 186 return err 187 } 188 var buildDependencies []buildinfo.Dependency 189 buildDependencies, err = serviceutils.ConvertArtifactsDetailsToBuildInfoDependencies(summary.ArtifactsDetailsReader) 190 if err != nil { 191 return err 192 } 193 populateFunc := func(partial *buildinfo.Partial) { 194 partial.Dependencies = buildDependencies 195 partial.ModuleId = dc.buildConfiguration.GetModule() 196 partial.ModuleType = buildinfo.Generic 197 } 198 return build.SavePartialBuildInfo(buildName, buildNumber, dc.buildConfiguration.GetProject(), populateFunc) 199 } 200 201 return err 202 } 203 204 func getDownloadParams(f *spec.File, configuration *utils.DownloadConfiguration) (downParams services.DownloadParams, err error) { 205 downParams = services.NewDownloadParams() 206 downParams.CommonParams, err = f.ToCommonParams() 207 if err != nil { 208 return 209 } 210 downParams.Symlink = configuration.Symlink 211 downParams.MinSplitSize = configuration.MinSplitSize 212 downParams.SplitCount = configuration.SplitCount 213 downParams.SkipChecksum = configuration.SkipChecksum 214 215 downParams.Recursive, err = f.IsRecursive(true) 216 if err != nil { 217 return 218 } 219 220 downParams.IncludeDirs, err = f.IsIncludeDirs(false) 221 if err != nil { 222 return 223 } 224 225 downParams.Flat, err = f.IsFlat(false) 226 if err != nil { 227 return 228 } 229 230 downParams.Explode, err = f.IsExplode(false) 231 if err != nil { 232 return 233 } 234 235 downParams.BypassArchiveInspection, err = f.IsBypassArchiveInspection(false) 236 if err != nil { 237 return 238 } 239 240 downParams.ValidateSymlink, err = f.IsValidateSymlinks(false) 241 if err != nil { 242 return 243 } 244 245 downParams.ExcludeArtifacts, err = f.IsExcludeArtifacts(false) 246 if err != nil { 247 return 248 } 249 250 downParams.IncludeDeps, err = f.IsIncludeDeps(false) 251 if err != nil { 252 return 253 } 254 255 downParams.Transitive, err = f.IsTransitive(false) 256 if err != nil { 257 return 258 } 259 260 downParams.PublicGpgKey = f.GetPublicGpgKey() 261 262 return 263 } 264 265 // We will create the same downloaded hierarchies under a temp directory with 0-size files. 266 // We will use this "empty reflection" of the download operation to determine whether a file was downloaded or not while walking the real filesystem from sync-deletes root. 267 func createDownloadResultEmptyTmpReflection(reader *content.ContentReader) (tmpRoot string, err error) { 268 tmpRoot, err = fileutils.CreateTempDir() 269 if errorutils.CheckError(err) != nil { 270 return 271 } 272 for path := new(clientutils.FileTransferDetails); reader.NextRecord(path) == nil; path = new(clientutils.FileTransferDetails) { 273 var absDownloadPath string 274 absDownloadPath, err = filepath.Abs(path.TargetPath) 275 if errorutils.CheckError(err) != nil { 276 return 277 } 278 legalPath := createLegalPath(tmpRoot, absDownloadPath) 279 tmpFileRoot := filepath.Dir(legalPath) 280 err = os.MkdirAll(tmpFileRoot, os.ModePerm) 281 if errorutils.CheckError(err) != nil { 282 return 283 } 284 var tmpFile *os.File 285 tmpFile, err = os.Create(legalPath) 286 if errorutils.CheckError(err) != nil { 287 return 288 } 289 err = tmpFile.Close() 290 if errorutils.CheckError(err) != nil { 291 return 292 } 293 } 294 return 295 } 296 297 // Creates absolute path for temp file suitable for all environments 298 func createLegalPath(root, path string) string { 299 // Avoid concatenating the volume name (e.g "C://") in Windows environment. 300 volumeName := filepath.VolumeName(path) 301 if volumeName != "" && strings.HasPrefix(path, volumeName) { 302 alternativeVolumeName := "VolumeName" + string(volumeName[0]) 303 path = strings.Replace(path, volumeName, alternativeVolumeName, 1) 304 } 305 // Join the current path to the temp root provided. 306 path = filepath.Join(root, path) 307 return path 308 } 309 310 func createSyncDeletesWalkFunction(tempRoot string) gofrog.WalkFunc { 311 return func(path string, info os.FileInfo, err error) error { 312 if err != nil { 313 return err 314 } 315 // Convert path to absolute path 316 path, err = filepath.Abs(path) 317 if errorutils.CheckError(err) != nil { 318 return err 319 } 320 pathToCheck := createLegalPath(tempRoot, path) 321 322 // If the path exists under the temp root directory, it means it's been downloaded during the last operations, and cannot be deleted. 323 if fileutils.IsPathExists(pathToCheck, false) { 324 return nil 325 } 326 log.Info("Deleting:", path) 327 if info.IsDir() { 328 // If current path is a dir - remove all content and return ErrSkipDir to stop walking this path 329 err = fileutils.RemoveTempDir(path) 330 if err == nil { 331 return gofrog.ErrSkipDir 332 } 333 } else { 334 // Path is a file 335 err = os.Remove(path) 336 } 337 338 return errorutils.CheckError(err) 339 } 340 }