Skip to content

Commit 2cbe70d

Browse files
authored
JFMIG-13 - Add ability to provide include files pattern to migrate only a part of repository (#1506)
1 parent f8481d1 commit 2cbe70d

File tree

12 files changed

+942
-7
lines changed

12 files changed

+942
-7
lines changed

artifactory/commands/transferfiles/filediff_test.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package transferfiles
22

33
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
47
"testing"
58

69
"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/api"
10+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/state"
11+
commonTests "github.com/jfrog/jfrog-cli-core/v2/common/tests"
712
servicesUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
813
"github.com/stretchr/testify/assert"
914
)
@@ -72,3 +77,171 @@ func TestGenerateDockerManifestAqlQuery(t *testing.T) {
7277
})
7378
}
7479
}
80+
81+
// TestGetNonDockerTimeFrameFilesDiffWithPatterns tests that getNonDockerTimeFrameFilesDiff uses pattern filtering when patterns are set
82+
func TestGetNonDockerTimeFrameFilesDiffWithPatterns(t *testing.T) {
83+
stateManager, cleanUp := state.InitStateTest(t)
84+
defer cleanUp()
85+
86+
aqlCalled := false
87+
receivedQuery := ""
88+
89+
mockAqlResults := servicesUtils.AqlSearchResult{
90+
Results: []servicesUtils.ResultItem{
91+
{Repo: "test-repo", Path: "org/company/projectA", Name: "file1.jar", Size: 100, Type: "file"},
92+
},
93+
}
94+
95+
testServer, serverDetails, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) {
96+
if r.RequestURI == "/api/search/aql" {
97+
aqlCalled = true
98+
// Read the query body
99+
buf := make([]byte, 1024)
100+
n, _ := r.Body.Read(buf)
101+
receivedQuery = string(buf[:n])
102+
103+
w.WriteHeader(http.StatusOK)
104+
response, _ := json.Marshal(mockAqlResults)
105+
_, _ = w.Write(response)
106+
}
107+
})
108+
defer testServer.Close()
109+
110+
assert.NoError(t, stateManager.SetRepoState("test-repo", 0, 0, false, true))
111+
112+
phase := &filesDiffPhase{
113+
phaseBase: phaseBase{
114+
context: context.Background(),
115+
stateManager: stateManager,
116+
repoKey: "test-repo",
117+
srcRtDetails: serverDetails,
118+
includeFilesPatterns: []string{"org/company/*"},
119+
},
120+
}
121+
122+
// Call getNonDockerTimeFrameFilesDiff with patterns
123+
result, err := phase.getNonDockerTimeFrameFilesDiff("2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z", 0)
124+
assert.NoError(t, err)
125+
assert.True(t, aqlCalled, "AQL should be called")
126+
assert.Len(t, result.Results, 1)
127+
128+
// Verify the query contains pattern filtering
129+
assert.Contains(t, receivedQuery, "$or", "Query should contain pattern $or condition")
130+
assert.Contains(t, receivedQuery, "org/company", "Query should contain the pattern")
131+
}
132+
133+
// TestGetNonDockerTimeFrameFilesDiffWithoutPatterns tests that getNonDockerTimeFrameFilesDiff uses default query when no patterns
134+
func TestGetNonDockerTimeFrameFilesDiffWithoutPatterns(t *testing.T) {
135+
stateManager, cleanUp := state.InitStateTest(t)
136+
defer cleanUp()
137+
138+
aqlCalled := false
139+
receivedQuery := ""
140+
141+
mockAqlResults := servicesUtils.AqlSearchResult{
142+
Results: []servicesUtils.ResultItem{
143+
{Repo: "test-repo", Path: "any/path", Name: "file1.jar", Size: 100, Type: "file"},
144+
},
145+
}
146+
147+
testServer, serverDetails, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) {
148+
if r.RequestURI == "/api/search/aql" {
149+
aqlCalled = true
150+
buf := make([]byte, 1024)
151+
n, _ := r.Body.Read(buf)
152+
receivedQuery = string(buf[:n])
153+
154+
w.WriteHeader(http.StatusOK)
155+
response, _ := json.Marshal(mockAqlResults)
156+
_, _ = w.Write(response)
157+
}
158+
})
159+
defer testServer.Close()
160+
161+
assert.NoError(t, stateManager.SetRepoState("test-repo", 0, 0, false, true))
162+
163+
phase := &filesDiffPhase{
164+
phaseBase: phaseBase{
165+
context: context.Background(),
166+
stateManager: stateManager,
167+
repoKey: "test-repo",
168+
srcRtDetails: serverDetails,
169+
includeFilesPatterns: []string{}, // No patterns
170+
},
171+
}
172+
173+
// Call getNonDockerTimeFrameFilesDiff without patterns
174+
result, err := phase.getNonDockerTimeFrameFilesDiff("2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z", 0)
175+
assert.NoError(t, err)
176+
assert.True(t, aqlCalled, "AQL should be called")
177+
assert.Len(t, result.Results, 1)
178+
179+
// Verify the query does NOT contain pattern filtering $or
180+
assert.NotContains(t, receivedQuery, `"$or":[{"path"`, "Query should not contain pattern $or condition when no patterns set")
181+
}
182+
183+
// TestGetDockerTimeFrameFilesDiffWithPatterns tests that getDockerTimeFrameFilesDiff uses pattern filtering when patterns are set
184+
func TestGetDockerTimeFrameFilesDiffWithPatterns(t *testing.T) {
185+
stateManager, cleanUp := state.InitStateTest(t)
186+
defer cleanUp()
187+
188+
aqlCallCount := 0
189+
firstQuery := ""
190+
191+
// First call returns manifest files, second call returns dir content
192+
testServer, serverDetails, _ := commonTests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) {
193+
if r.RequestURI == "/api/search/aql" {
194+
aqlCallCount++
195+
buf := make([]byte, 2048)
196+
n, _ := r.Body.Read(buf)
197+
query := string(buf[:n])
198+
199+
if aqlCallCount == 1 {
200+
// First query - capture for assertion
201+
firstQuery = query
202+
// Return manifest file
203+
mockAqlResults := servicesUtils.AqlSearchResult{
204+
Results: []servicesUtils.ResultItem{
205+
{Repo: "docker-repo", Path: "myapp/1.0", Name: "manifest.json", Size: 100, Type: "file"},
206+
},
207+
}
208+
w.WriteHeader(http.StatusOK)
209+
response, _ := json.Marshal(mockAqlResults)
210+
_, _ = w.Write(response)
211+
} else {
212+
// Second query - return dir content
213+
mockAqlResults := servicesUtils.AqlSearchResult{
214+
Results: []servicesUtils.ResultItem{
215+
{Repo: "docker-repo", Path: "myapp/1.0", Name: "layer1.tar", Size: 1000, Type: "file"},
216+
},
217+
}
218+
w.WriteHeader(http.StatusOK)
219+
response, _ := json.Marshal(mockAqlResults)
220+
_, _ = w.Write(response)
221+
}
222+
}
223+
})
224+
defer testServer.Close()
225+
226+
assert.NoError(t, stateManager.SetRepoState("docker-repo", 0, 0, false, true))
227+
228+
phase := &filesDiffPhase{
229+
phaseBase: phaseBase{
230+
context: context.Background(),
231+
stateManager: stateManager,
232+
repoKey: "docker-repo",
233+
srcRtDetails: serverDetails,
234+
includeFilesPatterns: []string{"myapp/*"},
235+
},
236+
}
237+
238+
// Call getDockerTimeFrameFilesDiff with patterns
239+
result, err := phase.getDockerTimeFrameFilesDiff("2024-01-01T00:00:00Z", "2024-01-02T00:00:00Z", 0)
240+
assert.NoError(t, err)
241+
assert.Equal(t, 2, aqlCallCount, "Two AQL calls should be made (manifest + dir content)")
242+
assert.Len(t, result.Results, 1)
243+
244+
// Verify the FIRST query contains pattern filtering and docker manifest names
245+
assert.Contains(t, firstQuery, "myapp", "First query should contain the pattern")
246+
assert.Contains(t, firstQuery, "manifest.json", "First query should contain manifest.json")
247+
}

artifactory/commands/transferfiles/filesdiff.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ func (f *filesDiffPhase) handleTimeFrameFilesDiff(pcWrapper *producerConsumerWra
125125
break
126126
}
127127
files := convertResultsToFileRepresentation(result)
128+
// Note: Pattern filtering is handled at AQL level for efficiency
128129
totalSize := 0
129130
for _, r := range files {
130131
totalSize += int(r.Size)
@@ -214,7 +215,14 @@ func (f *filesDiffPhase) getTimeFrameFilesDiff(fromTimestamp, toTimestamp string
214215
}
215216

216217
func (f *filesDiffPhase) getNonDockerTimeFrameFilesDiff(fromTimestamp, toTimestamp string, paginationOffset int) (aqlResult *servicesUtils.AqlSearchResult, err error) {
217-
query := generateDiffAqlQuery(f.repoKey, fromTimestamp, toTimestamp, paginationOffset, f.disabledDistinctiveAql)
218+
var query string
219+
if len(f.includeFilesPatterns) > 0 {
220+
// Use AQL with pattern filtering
221+
query = generateDiffAqlQueryWithPatterns(f.repoKey, fromTimestamp, toTimestamp, f.includeFilesPatterns, paginationOffset, f.disabledDistinctiveAql)
222+
} else {
223+
// Use default query without pattern filtering
224+
query = generateDiffAqlQuery(f.repoKey, fromTimestamp, toTimestamp, paginationOffset, f.disabledDistinctiveAql)
225+
}
218226
return runAql(f.context, f.srcRtDetails, query)
219227
}
220228

@@ -225,7 +233,12 @@ func (f *filesDiffPhase) getNonDockerTimeFrameFilesDiff(fromTimestamp, toTimesta
225233
// to get all artifacts in its path (that includes the "manifest.json" file itself and all its layouts).
226234
func (f *filesDiffPhase) getDockerTimeFrameFilesDiff(fromTimestamp, toTimestamp string, paginationOffset int) (aqlResult *servicesUtils.AqlSearchResult, err error) {
227235
// Get all newly created or modified manifest files ("manifest.json" and "list.manifest.json" files)
228-
query := generateDockerManifestAqlQuery(f.repoKey, fromTimestamp, toTimestamp, paginationOffset, f.disabledDistinctiveAql)
236+
var query string
237+
if len(f.includeFilesPatterns) > 0 {
238+
query = generateDockerManifestAqlQueryWithPatterns(f.repoKey, fromTimestamp, toTimestamp, f.includeFilesPatterns, paginationOffset, f.disabledDistinctiveAql)
239+
} else {
240+
query = generateDockerManifestAqlQuery(f.repoKey, fromTimestamp, toTimestamp, paginationOffset, f.disabledDistinctiveAql)
241+
}
229242
manifestFilesResult, err := runAql(f.context, f.srcRtDetails, query)
230243
if err != nil {
231244
return

artifactory/commands/transferfiles/fulltransfer.go

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package transferfiles
22

33
import (
44
"fmt"
5-
"github.com/jfrog/gofrog/safeconvert"
65
"path"
76
"time"
87

8+
"github.com/jfrog/gofrog/safeconvert"
9+
910
"github.com/jfrog/gofrog/parallel"
1011
"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/api"
1112
"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/state"
@@ -93,6 +94,19 @@ func (m *fullTransferPhase) skipPhase() {
9394

9495
func (m *fullTransferPhase) run() error {
9596
m.transferManager = newTransferManager(m.phaseBase, getDelayUploadComparisonFunctions(m.repoSummary.PackageType))
97+
98+
// If include patterns are provided, use AQL-based direct query instead of folder traversal
99+
if len(m.includeFilesPatterns) > 0 {
100+
return m.runWithAqlPatternFiltering()
101+
}
102+
103+
// Default: use folder traversal
104+
return m.runWithFolderTraversal()
105+
}
106+
107+
// runWithFolderTraversal uses the traditional folder-by-folder traversal approach.
108+
// This is the default behavior when no include patterns are specified.
109+
func (m *fullTransferPhase) runWithFolderTraversal() error {
96110
action := func(pcWrapper *producerConsumerWrapper, uploadChunkChan chan UploadedChunk, delayHelper delayUploadHelper, errorsChannelMng *ErrorsChannelMng) error {
97111
if ShouldStop(&m.phaseBase, &delayHelper, errorsChannelMng) {
98112
return nil
@@ -119,6 +133,75 @@ func (m *fullTransferPhase) run() error {
119133
return m.transferManager.doTransferWithProducerConsumer(action, delayAction)
120134
}
121135

136+
// runWithAqlPatternFiltering uses a direct AQL query to fetch all files matching the include patterns.
137+
// This is more efficient than folder traversal when filtering is needed.
138+
func (m *fullTransferPhase) runWithAqlPatternFiltering() error {
139+
log.Info("Using AQL-based pattern filtering for include patterns:", m.includeFilesPatterns)
140+
141+
action := func(pcWrapper *producerConsumerWrapper, uploadChunkChan chan UploadedChunk, delayHelper delayUploadHelper, errorsChannelMng *ErrorsChannelMng) error {
142+
if ShouldStop(&m.phaseBase, &delayHelper, errorsChannelMng) {
143+
return nil
144+
}
145+
146+
// Fetch all matching files using AQL with pattern filtering
147+
paginationOffset := 0
148+
for {
149+
if ShouldStop(&m.phaseBase, &delayHelper, errorsChannelMng) {
150+
return nil
151+
}
152+
153+
result, lastPage, err := m.getPatternMatchingFiles(paginationOffset)
154+
if err != nil {
155+
return err
156+
}
157+
158+
if len(result) == 0 {
159+
if paginationOffset == 0 {
160+
log.Info("No files found matching the include patterns")
161+
}
162+
break
163+
}
164+
165+
// Convert results to file representations and upload
166+
files := convertResultsToFileRepresentation(result)
167+
shouldStop, err := uploadByChunks(files, uploadChunkChan, m.phaseBase, delayHelper, errorsChannelMng, pcWrapper)
168+
if err != nil || shouldStop {
169+
return err
170+
}
171+
172+
if lastPage {
173+
break
174+
}
175+
paginationOffset++
176+
}
177+
return nil
178+
}
179+
180+
delayAction := func(phase phaseBase, addedDelayFiles []string) error {
181+
// Disable repo transfer snapshot as it is not used for delayed files.
182+
if err := m.stateManager.SaveStateAndSnapshots(); err != nil {
183+
return err
184+
}
185+
m.stateManager.DisableRepoTransferSnapshot()
186+
return consumeDelayFilesIfNoErrors(phase, addedDelayFiles)
187+
}
188+
189+
return m.transferManager.doTransferWithProducerConsumer(action, delayAction)
190+
}
191+
192+
// getPatternMatchingFiles fetches files from source Artifactory using AQL with pattern filtering.
193+
func (m *fullTransferPhase) getPatternMatchingFiles(paginationOffset int) (result []servicesUtils.ResultItem, lastPage bool, err error) {
194+
query := generatePatternBasedAqlQuery(m.repoKey, m.includeFilesPatterns, paginationOffset, m.disabledDistinctiveAql)
195+
aqlResults, err := runAql(m.context, m.srcRtDetails, query)
196+
if err != nil {
197+
return []servicesUtils.ResultItem{}, false, err
198+
}
199+
200+
lastPage = len(aqlResults.Results) < AqlPaginationLimit
201+
result, err = m.locallyGeneratedFilter.FilterLocallyGenerated(aqlResults.Results)
202+
return
203+
}
204+
122205
type folderFullTransferHandlerFunc func(params folderParams) parallel.TaskFunc
123206

124207
type folderParams struct {
@@ -241,6 +324,8 @@ func (m *fullTransferPhase) handleFoundChildFolder(params folderParams, pcWrappe
241324
return
242325
}
243326

327+
// Note: Pattern filtering is handled at AQL level when --include-files is provided.
328+
// This function is only called during folder traversal (when no patterns are specified).
244329
func (m *fullTransferPhase) handleFoundFile(pcWrapper *producerConsumerWrapper,
245330
uploadChunkChan chan UploadedChunk, delayHelper delayUploadHelper, errorsChannelMng *ErrorsChannelMng,
246331
node *reposnapshot.Node, item servicesUtils.ResultItem, curUploadChunk *api.UploadChunk) (err error) {

0 commit comments

Comments
 (0)