Skip to content

Commit

Permalink
Merge branch 'dev' into rbv2-ds-proj
Browse files Browse the repository at this point in the history
  • Loading branch information
RobiNino authored Mar 14, 2024
2 parents b6d8c52 + b652b7d commit e1a01ec
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 23 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
- [Deleting a Group](#deleting-a-group)
- [Generating Full System Export](#generating-full-system-export)
- [Getting Info of a Folder in Artifactory](#getting-info-of-a-folder-in-artifactory)
- [Getting Info of a File in Artifactory](#getting-info-of-a-file-in-artifactory)
- [Getting a listing of files and folders within a folder in Artifactory](#getting-a-listing-of-files-and-folders-within-a-folder-in-artifactory)
- [Getting Storage Summary Info of Artifactory](#getting-storage-summary-info-of-artifactory)
- [Triggering Storage Info Recalculation in Artifactory](#triggering-storage-info-recalculation-in-artifactory)
Expand Down Expand Up @@ -1353,6 +1354,12 @@ err := serviceManager.Export(params)
serviceManager.FolderInfo("repo/path/")
```

#### Getting Info of a File in Artifactory

```go
serviceManager.FileInfo("repo/path/file")
```

#### Getting a listing of files and folders within a folder in Artifactory

```go
Expand Down
5 changes: 5 additions & 0 deletions artifactory/emptymanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ type ArtifactoryServicesManager interface {
TriggerFederatedRepositoryFullSyncMirror(repoKey string, mirrorUrl string) error
Export(params services.ExportParams) error
FolderInfo(relativePath string) (*utils.FolderInfo, error)
FileInfo(relativePath string) (*utils.FileInfo, error)
FileList(relativePath string, optionalParams utils.FileListParams) (*utils.FileListResponse, error)
GetStorageInfo() (*utils.StorageInfo, error)
CalculateStorageInfo() error
Expand Down Expand Up @@ -448,6 +449,10 @@ func (esm *EmptyArtifactoryServicesManager) FolderInfo(string) (*utils.FolderInf
panic("Failed: Method is not implemented")
}

func (esm *EmptyArtifactoryServicesManager) FileInfo(string) (*utils.FileInfo, error) {
panic("Failed: Method is not implemented")
}

func (esm *EmptyArtifactoryServicesManager) FileList(string, utils.FileListParams) (*utils.FileListResponse, error) {
panic("Failed: Method is not implemented")
}
Expand Down
5 changes: 5 additions & 0 deletions artifactory/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,11 @@ func (sm *ArtifactoryServicesManagerImp) FolderInfo(relativePath string) (*utils
return storageService.FolderInfo(relativePath)
}

func (sm *ArtifactoryServicesManagerImp) FileInfo(relativePath string) (*utils.FileInfo, error) {
storageService := services.NewStorageService(sm.config.GetServiceDetails(), sm.client)
return storageService.FileInfo(relativePath)
}

func (sm *ArtifactoryServicesManagerImp) FileList(relativePath string, optionalParams utils.FileListParams) (*utils.FileListResponse, error) {
storageService := services.NewStorageService(sm.config.GetServiceDetails(), sm.client)
return storageService.FileList(relativePath, optionalParams)
Expand Down
2 changes: 2 additions & 0 deletions artifactory/services/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func (ds *DeleteService) createFileHandlerFunc(result *utils.Result) fileDeleteH
}
log.Info(logMsgPrefix+"Deleting", resultItem.GetItemRelativePath())
if ds.DryRun {
// Mock success count on dry run
result.SuccessCount[threadId]++
return nil
}
httpClientsDetails := ds.GetArtifactoryDetails().CreateHttpClientDetails()
Expand Down
31 changes: 25 additions & 6 deletions artifactory/services/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,46 @@ func (s *StorageService) GetJfrogHttpClient() *jfroghttpclient.JfrogHttpClient {
return s.client
}

func (s *StorageService) FileInfo(relativePath string) (*utils.FileInfo, error) {
body, err := s.getPathInfo(relativePath)
if err != nil {
return nil, err
}

result := &utils.FileInfo{}
err = json.Unmarshal(body, result)
return result, errorutils.CheckError(err)
}

func (s *StorageService) FolderInfo(relativePath string) (*utils.FolderInfo, error) {
body, err := s.getPathInfo(relativePath)
if err != nil {
return nil, err
}

result := &utils.FolderInfo{}
err = json.Unmarshal(body, result)
return result, errorutils.CheckError(err)
}

func (s *StorageService) getPathInfo(relativePath string) ([]byte, error) {
client := s.GetJfrogHttpClient()
restAPI := path.Join(StorageRestApi, path.Clean(relativePath))
folderUrl, err := clientutils.BuildUrl(s.GetArtifactoryDetails().GetUrl(), restAPI, make(map[string]string))
fullUrl, err := clientutils.BuildUrl(s.GetArtifactoryDetails().GetUrl(), restAPI, make(map[string]string))
if err != nil {
return nil, err
}

httpClientsDetails := s.GetArtifactoryDetails().CreateHttpClientDetails()
resp, body, _, err := client.SendGet(folderUrl, true, &httpClientsDetails)
resp, body, _, err := client.SendGet(fullUrl, true, &httpClientsDetails)
if err != nil {
return nil, err
}
if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK); err != nil {
return nil, err
}
log.Debug("Artifactory response:", resp.Status)

result := &utils.FolderInfo{}
err = json.Unmarshal(body, result)
return result, errorutils.CheckError(err)
return body, err
}

func (s *StorageService) FileList(relativePath string, optionalParams utils.FileListParams) (*utils.FileListResponse, error) {
Expand Down
16 changes: 11 additions & 5 deletions artifactory/services/utils/multipartupload.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ const (
aborted completionStatus = "ABORTED"

// API constants
uploadsApi = "/api/v1/uploads/"
artifactoryNodeIdHeader = "X-Artifactory-Node-Id"
uploadsApi = "/api/v1/uploads/"
routeToHeader = "X-JFrog-Route-To"
artifactoryNodeId = "X-Artifactory-Node-Id"

// Sizes and limits constants
MaxMultipartUploadFileSize = SizeTiB * 5
Expand Down Expand Up @@ -312,7 +313,7 @@ func (mu *MultipartUpload) completeAndPollForStatus(logMsgPrefix string, complet

func (mu *MultipartUpload) pollCompletionStatus(logMsgPrefix string, completionAttemptsLeft uint, sha1, nodeId string, multipartUploadClient *httputils.HttpClientDetails, progressReader ioutils.Progress) error {
multipartUploadClientWithNodeId := multipartUploadClient.Clone()
multipartUploadClientWithNodeId.Headers = map[string]string{artifactoryNodeIdHeader: nodeId}
multipartUploadClientWithNodeId.Headers = map[string]string{routeToHeader: nodeId}

lastMergeLog := time.Now()
pollingExecutor := &utils.RetryExecutor{
Expand Down Expand Up @@ -363,12 +364,17 @@ func (mu *MultipartUpload) completeMultipartUpload(logMsgPrefix, sha1 string, mu
return "", err
}
log.Debug("Artifactory response:", string(body), resp.Status)
return resp.Header.Get(artifactoryNodeIdHeader), errorutils.CheckResponseStatusWithBody(resp, body, http.StatusAccepted)
return resp.Header.Get(artifactoryNodeId), errorutils.CheckResponseStatusWithBody(resp, body, http.StatusAccepted)
}

func (mu *MultipartUpload) status(logMsgPrefix string, multipartUploadClientWithNodeId *httputils.HttpClientDetails) (status statusResponse, err error) {
url := fmt.Sprintf("%s%sstatus", mu.artifactoryUrl, uploadsApi)
resp, body, err := mu.client.GetHttpClient().SendPost(url, []byte{}, *multipartUploadClientWithNodeId, logMsgPrefix)
// If the Artifactory node returns a "Service unavailable" error (status 503), attempt to retry the upload completion process on a different node.
if resp != nil && resp.StatusCode == http.StatusServiceUnavailable {
unavailableNodeErr := fmt.Sprintf(logMsgPrefix + fmt.Sprintf("The Artifactory node ID '%s' is unavailable.", multipartUploadClientWithNodeId.Headers[routeToHeader]))
return statusResponse{Status: retryableError, Error: unavailableNodeErr}, nil
}
if err != nil {
return
}
Expand Down Expand Up @@ -421,7 +427,7 @@ func parseMultipartUploadStatus(status statusResponse) (shouldKeepPolling, shoul
return true, false, nil
case retryableError:
// Retryable error was received - stop polling and rerun the /complete API again
log.Warn("received error upon multipart upload completion process: '%s', retrying...", status.Error)
log.Warn(fmt.Printf("received error upon multipart upload completion process: '%s', retrying...", status.Error))
return false, true, nil
case finished, aborted:
// Upload finished or aborted
Expand Down
33 changes: 29 additions & 4 deletions artifactory/services/utils/multipartupload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func TestCompleteMultipartUpload(t *testing.T) {
assert.Equal(t, fmt.Sprintf("sha1=%s", sha1), r.URL.RawQuery)

// Add the "X-Artifactory-Node-Id" header to the response
w.Header().Add(artifactoryNodeIdHeader, nodeId)
w.Header().Add(artifactoryNodeId, nodeId)

// Send response 202 Accepted
w.WriteHeader(http.StatusAccepted)
Expand All @@ -211,8 +211,8 @@ func TestStatus(t *testing.T) {
// Check URL
assert.Equal(t, "/api/v1/uploads/status", r.URL.Path)

// Check "X-Artifactory-Node-Id" header
assert.Equal(t, nodeId, r.Header.Get(artifactoryNodeIdHeader))
// Check "X-JFrog-Route-To" header
assert.Equal(t, nodeId, r.Header.Get(routeToHeader))

// Send response 200 OK
w.WriteHeader(http.StatusOK)
Expand All @@ -227,12 +227,37 @@ func TestStatus(t *testing.T) {
defer cleanUp()

// Execute status
clientDetails := &httputils.HttpClientDetails{Headers: map[string]string{artifactoryNodeIdHeader: nodeId}}
clientDetails := &httputils.HttpClientDetails{Headers: map[string]string{routeToHeader: nodeId}}
status, err := multipartUpload.status("", clientDetails)
assert.NoError(t, err)
assert.Equal(t, statusResponse{Status: finished, Progress: utils.Pointer(100)}, status)
}

func TestStatusServiceUnavailable(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check method
assert.Equal(t, http.MethodPost, r.Method)

// Check URL
assert.Equal(t, "/api/v1/uploads/status", r.URL.Path)

// Send response 503 Service unavailable
w.WriteHeader(http.StatusServiceUnavailable)
_, err := w.Write([]byte("Service unavailable"))
assert.NoError(t, err)
})

// Create mock multipart upload with server
multipartUpload, cleanUp := createMockMultipartUpload(t, handler)
defer cleanUp()

// Execute status
clientDetails := &httputils.HttpClientDetails{Headers: map[string]string{routeToHeader: nodeId}}
status, err := multipartUpload.status("", clientDetails)
assert.NoError(t, err)
assert.Equal(t, statusResponse{Status: retryableError, Error: "The Artifactory node ID 'nodeId' is unavailable."}, status)
}

func TestAbort(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check method
Expand Down
20 changes: 20 additions & 0 deletions artifactory/services/utils/storageutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ const (
SizeTiB int64 = 1 << 40
)

type FileInfo struct {
Uri string `json:"uri,omitempty"`
DownloadUri string `json:"downloadUri,omitempty"`
Repo string `json:"repo,omitempty"`
Path string `json:"path,omitempty"`
RemoteUrl string `json:"remoteUrl,omitempty"`
Created string `json:"created,omitempty"`
CreatedBy string `json:"createdBy,omitempty"`
LastModified string `json:"lastModified,omitempty"`
ModifiedBy string `json:"modifiedBy,omitempty"`
LastUpdated string `json:"lastUpdated,omitempty"`
Size string `json:"size,omitempty"`
MimeType string `json:"mimeType,omitempty"`
Checksums struct {
Sha1 string `json:"sha1,omitempty"`
Sha256 string `json:"sha256,omitempty"`
Md5 string `json:"md5,omitempty"`
} `json:"checksums,omitempty"`
}

type FolderInfo struct {
Uri string `json:"uri,omitempty"`
Repo string `json:"repo,omitempty"`
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/gookit/color v1.5.4
github.com/jfrog/archiver/v3 v3.6.0
github.com/jfrog/build-info-go v1.9.23
github.com/jfrog/gofrog v1.6.0
github.com/jfrog/gofrog v1.6.3
github.com/stretchr/testify v1.8.4
github.com/xanzy/ssh-agent v0.3.3
golang.org/x/crypto v0.19.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ github.com/jfrog/archiver/v3 v3.6.0 h1:OVZ50vudkIQmKMgA8mmFF9S0gA47lcag22N13iV3F
github.com/jfrog/archiver/v3 v3.6.0/go.mod h1:fCAof46C3rAXgZurS8kNRNdSVMKBbZs+bNNhPYxLldI=
github.com/jfrog/build-info-go v1.8.9-0.20240225113943-096bf22ca54c h1:M1QiuCYGCYN1IiGyxogrLzfetYGkkhE2pgDh5K4Wo9A=
github.com/jfrog/build-info-go v1.8.9-0.20240225113943-096bf22ca54c/go.mod h1:QHcKuesY4MrBVBuEwwBz4uIsX6mwYuMEDV09ng4AvAU=
github.com/jfrog/gofrog v1.6.0 h1:jOwb37nHY2PnxePNFJ6e6279Pgkr3di05SbQQw47Mq8=
github.com/jfrog/gofrog v1.6.0/go.mod h1:SZ1EPJUruxrVGndOzHd+LTiwWYKMlHqhKD+eu+v5Hqg=
github.com/jfrog/gofrog v1.6.3 h1:F7He0+75HcgCe6SGTSHLFCBDxiE2Ja0tekvvcktW6wc=
github.com/jfrog/gofrog v1.6.3/go.mod h1:SZ1EPJUruxrVGndOzHd+LTiwWYKMlHqhKD+eu+v5Hqg=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
Expand Down
26 changes: 21 additions & 5 deletions tests/artifactorystorage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,29 @@ package tests

import (
"errors"
"github.com/jfrog/jfrog-client-go/artifactory/services"
servicesutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
"github.com/jfrog/jfrog-client-go/utils"
"github.com/stretchr/testify/assert"
"path"
"strings"
"testing"
"time"

"github.com/jfrog/jfrog-client-go/artifactory/services"
servicesutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
"github.com/jfrog/jfrog-client-go/utils"
"github.com/stretchr/testify/assert"
)

func TestArtifactoryStorage(t *testing.T) {
initArtifactoryTest(t)
uploadDummyFile(t)
t.Run("folder info test", folderInfoTest)
t.Run("file info test", fileInfoTest)
t.Run("file list test", fileListTest)
t.Run("storage info test", storageInfoTest)

artifactoryCleanup(t)
}

func fileInfoTest(t *testing.T) {
func folderInfoTest(t *testing.T) {
info, err := testsStorageService.FolderInfo(getRtTargetRepo() + "test/")
if !assert.NoError(t, err) {
return
Expand All @@ -39,6 +41,20 @@ func fileInfoTest(t *testing.T) {
assert.False(t, info.Children[0].Folder)
}

func fileInfoTest(t *testing.T) {
info, err := testsStorageService.FileInfo(getRtTargetRepo() + "test/a.in")
if !assert.NoError(t, err) {
return
}
assert.Equal(t, utils.AddTrailingSlashIfNeeded(*RtUrl)+path.Join(services.StorageRestApi, getRtTargetRepo()+"test/a.in"), info.Uri)
assert.Equal(t, strings.TrimSuffix(getRtTargetRepo(), "/"), info.Repo)
assert.Equal(t, "/test/a.in", info.Path)
assert.NotEmpty(t, info.Created)
assert.NotEmpty(t, info.CreatedBy)
assert.NotEmpty(t, info.LastModified)
assert.NotEmpty(t, info.LastUpdated)
}

func fileListTest(t *testing.T) {
params := servicesutils.NewFileListParams()
params.Deep = true
Expand Down

0 comments on commit e1a01ec

Please sign in to comment.