github.com/osievert/jfrog-cli-core@v1.2.7/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 "github.com/jfrog/jfrog-cli-core/artifactory/spec" 11 "github.com/jfrog/jfrog-cli-core/artifactory/utils" 12 "github.com/jfrog/jfrog-client-go/artifactory/buildinfo" 13 "github.com/jfrog/jfrog-client-go/artifactory/services" 14 clientutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" 15 "github.com/jfrog/jfrog-client-go/utils/errorutils" 16 ioUtils "github.com/jfrog/jfrog-client-go/utils/io" 17 "github.com/jfrog/jfrog-client-go/utils/io/content" 18 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 19 "github.com/jfrog/jfrog-client-go/utils/log" 20 ) 21 22 type DownloadCommand struct { 23 buildConfiguration *utils.BuildConfiguration 24 GenericCommand 25 configuration *utils.DownloadConfiguration 26 progress ioUtils.ProgressMgr 27 } 28 29 func NewDownloadCommand() *DownloadCommand { 30 return &DownloadCommand{GenericCommand: *NewGenericCommand()} 31 } 32 33 func (dc *DownloadCommand) SetBuildConfiguration(buildConfiguration *utils.BuildConfiguration) *DownloadCommand { 34 dc.buildConfiguration = buildConfiguration 35 return dc 36 } 37 38 func (dc *DownloadCommand) Configuration() *utils.DownloadConfiguration { 39 return dc.configuration 40 } 41 42 func (dc *DownloadCommand) SetConfiguration(configuration *utils.DownloadConfiguration) *DownloadCommand { 43 dc.configuration = configuration 44 return dc 45 } 46 47 func (dc *DownloadCommand) SetProgress(progress ioUtils.ProgressMgr) { 48 dc.progress = progress 49 } 50 51 func (dc *DownloadCommand) ShouldPrompt() bool { 52 return !dc.DryRun() && dc.SyncDeletesPath() != "" && !dc.Quiet() 53 } 54 55 func (dc *DownloadCommand) CommandName() string { 56 return "rt_download" 57 } 58 59 func (dc *DownloadCommand) Run() error { 60 return dc.download() 61 } 62 63 func (dc *DownloadCommand) download() error { 64 // Create Service Manager: 65 servicesManager, err := utils.CreateDownloadServiceManager(dc.rtDetails, dc.configuration.Threads, dc.DryRun(), dc.progress) 66 if err != nil { 67 return err 68 } 69 70 // Build Info Collection: 71 isCollectBuildInfo := len(dc.buildConfiguration.BuildName) > 0 && len(dc.buildConfiguration.BuildNumber) > 0 72 if isCollectBuildInfo && !dc.DryRun() { 73 if err = utils.SaveBuildGeneralDetails(dc.buildConfiguration.BuildName, dc.buildConfiguration.BuildNumber); err != nil { 74 return err 75 } 76 } 77 78 var errorOccurred = false 79 var downloadParamsArray []services.DownloadParams 80 // Create DownloadParams for all File-Spec groups. 81 for i := 0; i < len(dc.Spec().Files); i++ { 82 downParams, err := getDownloadParams(dc.Spec().Get(i), dc.configuration) 83 if err != nil { 84 errorOccurred = true 85 log.Error(err) 86 continue 87 } 88 downloadParamsArray = append(downloadParamsArray, downParams) 89 } 90 // Perform download. 91 // In case of build-info collection/sync-deletes operation/a detailed summary is required, we use the download service which provides results file reader, 92 // otherwise we use the download service which provides only general counters. 93 var totalDownloaded, totalExpected int 94 var resultsReader *content.ContentReader = nil 95 if isCollectBuildInfo || dc.SyncDeletesPath() != "" || dc.DetailedSummary() { 96 resultsReader, totalDownloaded, totalExpected, err = servicesManager.DownloadFilesWithResultReader(downloadParamsArray...) 97 dc.result.SetReader(resultsReader) 98 } else { 99 totalDownloaded, totalExpected, err = servicesManager.DownloadFiles(downloadParamsArray...) 100 } 101 if err != nil { 102 errorOccurred = true 103 log.Error(err) 104 } 105 dc.result.SetSuccessCount(totalDownloaded) 106 dc.result.SetFailCount(totalExpected - totalDownloaded) 107 // If the 'details summary' was requested, then the reader should not be closed now. 108 // It will be closed after it will be used to generate the summary. 109 if resultsReader != nil && !dc.DetailedSummary() { 110 defer func() { 111 resultsReader.Close() 112 dc.result.SetReader(nil) 113 }() 114 } 115 // Check for errors. 116 if errorOccurred { 117 return errors.New("Download finished with errors, please review the logs.") 118 } 119 if dc.DryRun() { 120 dc.result.SetSuccessCount(totalExpected) 121 dc.result.SetFailCount(0) 122 return err 123 } else if dc.SyncDeletesPath() != "" { 124 absSyncDeletesPath, err := filepath.Abs(dc.SyncDeletesPath()) 125 if err != nil { 126 return errorutils.CheckError(err) 127 } 128 if _, err = os.Stat(absSyncDeletesPath); err == nil { 129 // Unmarshal the local paths of the downloaded files from the results file reader 130 tmpRoot, err := createDownloadResultEmptyTmpReflection(resultsReader) 131 defer fileutils.RemoveTempDir(tmpRoot) 132 if err != nil { 133 return err 134 } 135 walkFn := createSyncDeletesWalkFunction(tmpRoot) 136 err = fileutils.Walk(dc.SyncDeletesPath(), walkFn, false) 137 if err != nil { 138 return errorutils.CheckError(err) 139 } 140 } else if os.IsNotExist(err) { 141 log.Info("Sync-deletes path", absSyncDeletesPath, "does not exists.") 142 } 143 } 144 log.Debug("Downloaded", strconv.Itoa(totalDownloaded), "artifacts.") 145 146 // Build Info 147 if isCollectBuildInfo { 148 err, resultBuildInfo := utils.ReadResultBuildInfo(resultsReader) 149 if err != nil { 150 return err 151 } 152 buildDependencies := convertFileInfoToBuildDependencies(resultBuildInfo.FilesInfo) 153 populateFunc := func(partial *buildinfo.Partial) { 154 partial.Dependencies = buildDependencies 155 partial.ModuleId = dc.buildConfiguration.Module 156 partial.ModuleType = buildinfo.Generic 157 } 158 err = utils.SavePartialBuildInfo(dc.buildConfiguration.BuildName, dc.buildConfiguration.BuildNumber, populateFunc) 159 } 160 161 return err 162 } 163 164 func convertFileInfoToBuildDependencies(filesInfo []clientutils.FileInfo) []buildinfo.Dependency { 165 buildDependencies := make([]buildinfo.Dependency, len(filesInfo)) 166 for i, fileInfo := range filesInfo { 167 dependency := buildinfo.Dependency{Checksum: &buildinfo.Checksum{}} 168 dependency.Md5 = fileInfo.Md5 169 dependency.Sha1 = fileInfo.Sha1 170 // Artifact name in build info as the name in artifactory 171 filename, _ := fileutils.GetFileAndDirFromPath(fileInfo.ArtifactoryPath) 172 dependency.Id = filename 173 buildDependencies[i] = dependency 174 } 175 return buildDependencies 176 } 177 178 func getDownloadParams(f *spec.File, configuration *utils.DownloadConfiguration) (downParams services.DownloadParams, err error) { 179 downParams = services.NewDownloadParams() 180 downParams.ArtifactoryCommonParams = f.ToArtifactoryCommonParams() 181 downParams.Symlink = configuration.Symlink 182 downParams.MinSplitSize = configuration.MinSplitSize 183 downParams.SplitCount = configuration.SplitCount 184 downParams.Retries = configuration.Retries 185 186 downParams.Recursive, err = f.IsRecursive(true) 187 if err != nil { 188 return 189 } 190 191 downParams.IncludeDirs, err = f.IsIncludeDirs(false) 192 if err != nil { 193 return 194 } 195 196 downParams.Flat, err = f.IsFlat(false) 197 if err != nil { 198 return 199 } 200 201 downParams.Explode, err = f.IsExplode(false) 202 if err != nil { 203 return 204 } 205 206 downParams.ValidateSymlink, err = f.IsVlidateSymlinks(false) 207 if err != nil { 208 return 209 } 210 211 downParams.ExcludeArtifacts, err = f.IsExcludeArtifacts(false) 212 if err != nil { 213 return 214 } 215 216 downParams.IncludeDeps, err = f.IsIncludeDeps(false) 217 if err != nil { 218 return 219 } 220 221 return 222 } 223 224 // We will create the same downloaded hierarchies under a temp directory with 0-size files. 225 // 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. 226 func createDownloadResultEmptyTmpReflection(reader *content.ContentReader) (tmpRoot string, err error) { 227 tmpRoot, err = fileutils.CreateTempDir() 228 if errorutils.CheckError(err) != nil { 229 return 230 } 231 for path := new(localPath); reader.NextRecord(path) == nil; path = new(localPath) { 232 var absDownloadPath string 233 absDownloadPath, err = filepath.Abs(path.LocalPath) 234 if errorutils.CheckError(err) != nil { 235 return 236 } 237 legalPath := createLegalPath(tmpRoot, absDownloadPath) 238 tmpFileRoot := filepath.Dir(legalPath) 239 err = os.MkdirAll(tmpFileRoot, os.ModePerm) 240 if errorutils.CheckError(err) != nil { 241 return 242 } 243 var tmpFile *os.File 244 tmpFile, err = os.Create(legalPath) 245 if errorutils.CheckError(err) != nil { 246 return 247 } 248 err = tmpFile.Close() 249 if errorutils.CheckError(err) != nil { 250 return 251 } 252 } 253 return 254 } 255 256 // Creates absolute path for temp file suitable for all environments 257 func createLegalPath(root, path string) string { 258 // Avoid concatenating the volume name (e.g "C://") in Windows environment. 259 volumeName := filepath.VolumeName(path) 260 if volumeName != "" && strings.HasPrefix(path, volumeName) { 261 alternativeVolumeName := "VolumeName" + string(volumeName[0]) 262 path = strings.Replace(path, volumeName, alternativeVolumeName, 1) 263 } 264 // Join the current path to the temp root provided. 265 path = filepath.Join(root, path) 266 return path 267 } 268 269 func createSyncDeletesWalkFunction(tempRoot string) fileutils.WalkFunc { 270 return func(path string, info os.FileInfo, err error) error { 271 // Convert path to absolute path 272 path, err = filepath.Abs(path) 273 if errorutils.CheckError(err) != nil { 274 return err 275 } 276 pathToCheck := createLegalPath(tempRoot, path) 277 278 // If the path exists under the temp root directory, it means it's been downloaded during the last operations, and cannot be deleted. 279 if fileutils.IsPathExists(pathToCheck, false) { 280 return nil 281 } 282 log.Info("Deleting:", path) 283 if info.IsDir() { 284 // If current path is a dir - remove all content and return SkipDir to stop walking this path 285 err = os.RemoveAll(path) 286 if err == nil { 287 return fileutils.SkipDir 288 } 289 } else { 290 // Path is a file 291 err = os.Remove(path) 292 } 293 294 return errorutils.CheckError(err) 295 } 296 } 297 298 type localPath struct { 299 LocalPath string `json:"localPath,omitempty"` 300 }