github.com/cobalt77/jfrog-client-go@v0.14.5/bintray/services/download.go (about)

     1  package services
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"net/http"
     7  	"path"
     8  	"path/filepath"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/cobalt77/jfrog-client-go/bintray/auth"
    14  	"github.com/cobalt77/jfrog-client-go/bintray/services/utils"
    15  	"github.com/cobalt77/jfrog-client-go/bintray/services/versions"
    16  	"github.com/cobalt77/jfrog-client-go/httpclient"
    17  	clientutils "github.com/cobalt77/jfrog-client-go/utils"
    18  	logutil "github.com/cobalt77/jfrog-client-go/utils"
    19  	"github.com/cobalt77/jfrog-client-go/utils/errorutils"
    20  	"github.com/cobalt77/jfrog-client-go/utils/io/fileutils"
    21  	"github.com/cobalt77/jfrog-client-go/utils/log"
    22  )
    23  
    24  func NewDownloadService(client *httpclient.HttpClient) *DownloadService {
    25  	ds := &DownloadService{client: client}
    26  	return ds
    27  }
    28  
    29  func NewDownloadFileParams() *DownloadFileParams {
    30  	return &DownloadFileParams{PathDetails: &utils.PathDetails{}}
    31  }
    32  
    33  func NewDownloadVersionParams() *DownloadVersionParams {
    34  	return &DownloadVersionParams{Params: &versions.Params{}}
    35  }
    36  
    37  type DownloadService struct {
    38  	client         *httpclient.HttpClient
    39  	BintrayDetails auth.BintrayDetails
    40  	Threads        int
    41  }
    42  
    43  type DownloadFileParams struct {
    44  	*utils.PathDetails
    45  	TargetPath         string
    46  	IncludeUnpublished bool
    47  	Flat               bool
    48  	MinSplitSize       int64
    49  	SplitCount         int
    50  }
    51  
    52  type DownloadVersionParams struct {
    53  	*versions.Params
    54  	TargetPath         string
    55  	IncludeUnpublished bool
    56  }
    57  
    58  func (ds *DownloadService) DownloadFile(downloadParams *DownloadFileParams) (totalDownloaded, totalFailed int, err error) {
    59  	if ds.BintrayDetails.GetUser() == "" {
    60  		ds.BintrayDetails.SetUser(downloadParams.Subject)
    61  	}
    62  
    63  	err = ds.downloadBintrayFile(downloadParams, "")
    64  	if err != nil {
    65  		return 0, 1, err
    66  	}
    67  	log.Info("Downloaded 1 artifact.")
    68  	return 1, 0, nil
    69  }
    70  
    71  func (ds *DownloadService) DownloadVersion(downloadParams *DownloadVersionParams) (totalDownloaded, totalFailed int, err error) {
    72  	versionPathUrl := buildDownloadVersionUrl(ds.BintrayDetails.GetApiUrl(), downloadParams)
    73  	httpClientsDetails := ds.BintrayDetails.CreateHttpClientDetails()
    74  	if httpClientsDetails.User == "" {
    75  		httpClientsDetails.User = downloadParams.Subject
    76  	}
    77  	client, err := httpclient.ClientBuilder().Build()
    78  	if err != nil {
    79  		return
    80  	}
    81  	resp, body, _, _ := client.SendGet(versionPathUrl, true, httpClientsDetails)
    82  	if resp.StatusCode != http.StatusOK {
    83  		err = errorutils.CheckError(errors.New(resp.Status + ". " + utils.ReadBintrayMessage(body)))
    84  		return
    85  	}
    86  	var files []VersionFilesResult
    87  	err = json.Unmarshal(body, &files)
    88  	if errorutils.CheckError(err) != nil {
    89  		return
    90  	}
    91  
    92  	totalDownloaded, err = ds.downloadVersionFiles(files, downloadParams)
    93  	log.Info("Downloaded", strconv.Itoa(totalDownloaded), "artifacts.")
    94  	totalFailed = len(files) - totalDownloaded
    95  	return
    96  }
    97  
    98  func buildDownloadVersionUrl(apiUrl string, downloadParams *DownloadVersionParams) string {
    99  	urlPath := apiUrl + path.Join("packages/", downloadParams.Subject, downloadParams.Repo, downloadParams.Package, "versions", downloadParams.Version, "files")
   100  	if downloadParams.IncludeUnpublished {
   101  		urlPath += "?include_unpublished=1"
   102  	}
   103  	return urlPath
   104  }
   105  
   106  func (ds *DownloadService) downloadVersionFiles(files []VersionFilesResult, downloadParams *DownloadVersionParams) (totalDownloaded int, err error) {
   107  	size := len(files)
   108  	downloadedForThread := make([]int, ds.Threads)
   109  	var wg sync.WaitGroup
   110  	for i := 0; i < ds.Threads; i++ {
   111  		wg.Add(1)
   112  		go func(threadId int) {
   113  			logMsgPrefix := logutil.GetLogMsgPrefix(threadId, false)
   114  			for j := threadId; j < size; j += ds.Threads {
   115  				pathDetails := &utils.PathDetails{
   116  					Subject: downloadParams.Subject,
   117  					Repo:    downloadParams.Repo,
   118  					Path:    files[j].Path}
   119  
   120  				downloadFileParams := &DownloadFileParams{PathDetails: pathDetails, TargetPath: downloadParams.TargetPath}
   121  				e := ds.downloadBintrayFile(downloadFileParams, logMsgPrefix)
   122  				if e != nil {
   123  					err = e
   124  					continue
   125  				}
   126  				downloadedForThread[threadId]++
   127  			}
   128  			wg.Done()
   129  		}(i)
   130  	}
   131  	wg.Wait()
   132  
   133  	for i := range downloadedForThread {
   134  		totalDownloaded += downloadedForThread[i]
   135  	}
   136  	return
   137  }
   138  
   139  func CreateVersionDetailsForDownloadVersion(versionStr string) (*versions.Path, error) {
   140  	parts := strings.Split(versionStr, "/")
   141  	if len(parts) != 4 {
   142  		err := errorutils.CheckError(errors.New("Argument format should be subject/repository/package/version. Got " + versionStr))
   143  		if err != nil {
   144  			return nil, err
   145  		}
   146  	}
   147  	return versions.CreatePath(versionStr)
   148  }
   149  
   150  type VersionFilesResult struct {
   151  	Path string
   152  }
   153  
   154  func (ds *DownloadService) downloadBintrayFile(downloadParams *DownloadFileParams, logMsgPrefix string) error {
   155  	cleanPath := strings.Replace(downloadParams.Path, "(", "", -1)
   156  	cleanPath = strings.Replace(cleanPath, ")", "", -1)
   157  	downloadPath := path.Join(downloadParams.Subject, downloadParams.Repo, cleanPath)
   158  
   159  	fileName, filePath := fileutils.GetFileAndDirFromPath(cleanPath)
   160  
   161  	url := ds.BintrayDetails.GetDownloadServerUrl() + downloadPath
   162  	if downloadParams.IncludeUnpublished {
   163  		url += "?include_unpublished=1"
   164  	}
   165  	log.Info(logMsgPrefix+"Downloading", downloadPath)
   166  	client, err := httpclient.ClientBuilder().Build()
   167  	if err != nil {
   168  		return err
   169  	}
   170  
   171  	httpClientsDetails := ds.BintrayDetails.CreateHttpClientDetails()
   172  	details, resp, err := client.GetRemoteFileDetails(url, httpClientsDetails)
   173  	if err != nil {
   174  		return errorutils.CheckError(errors.New("Bintray " + err.Error()))
   175  	}
   176  	err = errorutils.CheckResponseStatus(resp, http.StatusOK)
   177  	if errorutils.CheckError(err) != nil {
   178  		return err
   179  	}
   180  
   181  	placeHolderTarget, err := clientutils.BuildTargetPath(downloadParams.Path, cleanPath, downloadParams.TargetPath, false)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	localPath, localFileName := fileutils.GetLocalPathAndFile(fileName, filePath, placeHolderTarget, downloadParams.Flat)
   187  
   188  	var shouldDownload bool
   189  	shouldDownload, err = shouldDownloadFile(filepath.Join(localPath, localFileName), details)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	if !shouldDownload {
   194  		log.Info(logMsgPrefix, "File already exists locally.")
   195  		return nil
   196  	}
   197  
   198  	// Check if the file should be downloaded concurrently.
   199  	if downloadParams.SplitCount == 0 || downloadParams.MinSplitSize < 0 || downloadParams.MinSplitSize*1000 > details.Size {
   200  		// File should not be downloaded concurrently. Download it as one block.
   201  		downloadDetails := &httpclient.DownloadFileDetails{
   202  			FileName:      fileName,
   203  			DownloadPath:  url,
   204  			LocalPath:     localPath,
   205  			LocalFileName: localFileName}
   206  
   207  		resp, err := client.DownloadFile(downloadDetails, logMsgPrefix, httpClientsDetails, utils.BintrayDownloadRetries, false)
   208  		if err != nil {
   209  			return err
   210  		}
   211  		log.Debug(logMsgPrefix, "Bintray response:", resp.Status)
   212  		return errorutils.CheckResponseStatus(resp, http.StatusOK)
   213  	} else {
   214  		// We should attempt to download the file concurrently, but only if it is provided through the DSN.
   215  		// To check if the file is provided through the DSN, we first attempt to download the file
   216  		// with 'follow redirect' disabled.
   217  
   218  		var resp *http.Response
   219  		var redirectUrl string
   220  		resp, redirectUrl, err =
   221  			client.DownloadFileNoRedirect(url, localPath, localFileName, httpClientsDetails, utils.BintrayDownloadRetries)
   222  		// There are two options now. Either the file has just been downloaded as one block, or
   223  		// we got a redirect to DSN download URL. In case of the later, we should download the file
   224  		// concurrently from the DSN URL.
   225  		// 'err' is not nil in case 'redirectUrl' was returned.
   226  		if redirectUrl != "" {
   227  			err = nil
   228  			concurrentDownloadFlags := httpclient.ConcurrentDownloadFlags{
   229  				DownloadPath:  redirectUrl,
   230  				FileName:      localFileName,
   231  				LocalFileName: localFileName,
   232  				LocalPath:     localPath,
   233  				FileSize:      details.Size,
   234  				SplitCount:    downloadParams.SplitCount,
   235  				Retries:       utils.BintrayDownloadRetries}
   236  
   237  			resp, err = client.DownloadFileConcurrently(concurrentDownloadFlags, "", httpClientsDetails, nil)
   238  			if errorutils.CheckError(err) != nil {
   239  				return err
   240  			}
   241  			err = errorutils.CheckResponseStatus(resp, http.StatusPartialContent)
   242  			if err != nil {
   243  				return err
   244  			}
   245  		} else {
   246  			if errorutils.CheckError(err) != nil {
   247  				return err
   248  			}
   249  			err = errorutils.CheckResponseStatus(resp, http.StatusOK)
   250  			if err != nil {
   251  				return err
   252  			}
   253  			log.Debug(logMsgPrefix, "Bintray response:", resp.Status)
   254  		}
   255  	}
   256  	return nil
   257  }
   258  
   259  func shouldDownloadFile(localFilePath string, remoteFileDetails *fileutils.FileDetails) (bool, error) {
   260  	exists, err := fileutils.IsFileExists(localFilePath, false)
   261  	if err != nil {
   262  		return false, err
   263  	}
   264  	if !exists {
   265  		return true, nil
   266  	}
   267  	localFileDetails, err := fileutils.GetFileDetails(localFilePath)
   268  	if err != nil {
   269  		return false, err
   270  	}
   271  	return localFileDetails.Checksum.Sha1 != remoteFileDetails.Checksum.Sha1, nil
   272  }