Skip to content

Commit d8066f4

Browse files
authored
[artefacts] Extend artefact download to retain the tree structure of the artefacts (#88)
<!-- Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors. All rights reserved. SPDX-License-Identifier: Proprietary --> ### Description - This is so that artefacts tree is automatically reproduced in client machine ### Test Coverage <!-- Please put an `x` in the correct box e.g. `[x]` to indicate the testing coverage of this change. --> - [x] This change is covered by existing or additional automated tests. - [ ] Manual testing has been performed (and evidence provided) as automated testing was not feasible. - [ ] Additional tests are not required for this change (e.g. documentation update).
1 parent 402057d commit d8066f4

File tree

9 files changed

+271
-39
lines changed

9 files changed

+271
-39
lines changed

changes/20241115120136.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:arrow_up: upgrade dependencies

changes/20241115141349.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `[artefacts]` Extend artefact download to retain the tree structure of the artefacts

utils/artefacts/artefacts.go

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net/url"
1313
"os"
1414
"path/filepath"
15+
"strings"
1516

1617
"github.com/ARM-software/embedded-development-services-client-utils/utils/api"
1718
paginationUtils "github.com/ARM-software/embedded-development-services-client-utils/utils/pagination"
@@ -24,6 +25,8 @@ import (
2425
"github.com/ARM-software/golang-utils/utils/safeio"
2526
)
2627

28+
const relativePathKey = "Relative Path"
29+
2730
type (
2831
// GetArtefactManagersFirstPageFunc defines the function which can retrieve the first page of artefact managers.
2932
GetArtefactManagersFirstPageFunc = func(ctx context.Context, job string) (*client.ArtefactManagerCollection, *http.Response, error)
@@ -35,6 +38,44 @@ type (
3538
GetArtefactContentFunc = func(ctx context.Context, job, artefactID string) (*os.File, *http.Response, error)
3639
)
3740

41+
func determineArtefactDestination(outputDir string, maintainTree bool, item *client.ArtefactManagerItem) (artefactFileName string, destinationDir string, err error) {
42+
if item == nil {
43+
err = fmt.Errorf("%w: missing artefact item", commonerrors.ErrUndefined)
44+
return
45+
}
46+
artefactManagerName := item.GetName()
47+
if artefactManagerName == "" {
48+
err = fmt.Errorf("%w: missing artefact name", commonerrors.ErrUndefined)
49+
return
50+
}
51+
rawFileName := artefactManagerName
52+
if item.HasTitle() {
53+
rawFileName = item.GetTitle()
54+
}
55+
artefactFileName = rawFileName
56+
if unescapedName, err := url.PathUnescape(rawFileName); err == nil {
57+
artefactFileName = unescapedName
58+
}
59+
destinationDir = filepath.Clean(outputDir)
60+
if !maintainTree {
61+
return
62+
}
63+
64+
if item.HasExtraMetadata() {
65+
m := item.GetExtraMetadata()
66+
treePath, ok := m[relativePathKey]
67+
if !ok {
68+
return
69+
}
70+
treePath = strings.TrimSpace(treePath)
71+
if strings.HasSuffix(treePath, rawFileName) || strings.HasSuffix(treePath, artefactFileName) {
72+
treePath = filepath.Dir(treePath)
73+
}
74+
destinationDir = filepath.Clean(filepath.Join(outputDir, treePath))
75+
}
76+
return
77+
}
78+
3879
type ArtefactManager struct {
3980
getArtefactManagerFunc GetArtefactManagerFunc
4081
getArtefactContentFunc GetArtefactContentFunc
@@ -51,8 +92,11 @@ func NewArtefactManager(getArtefactManagersFirstPage GetArtefactManagersFirstPag
5192
getArtefactManagersFollowLinkFunc: getArtefactsManagersPage,
5293
}
5394
}
54-
5595
func (m *ArtefactManager) DownloadJobArtefact(ctx context.Context, jobName string, outputDirectory string, artefactManager *client.ArtefactManagerItem) (err error) {
96+
return m.DownloadJobArtefactWithTree(ctx, jobName, false, outputDirectory, artefactManager)
97+
}
98+
99+
func (m *ArtefactManager) DownloadJobArtefactWithTree(ctx context.Context, jobName string, maintainTreeLocation bool, outputDirectory string, artefactManager *client.ArtefactManagerItem) (err error) {
56100
err = parallelisation.DetermineContextError(ctx)
57101
if err != nil {
58102
return
@@ -83,14 +127,6 @@ func (m *ArtefactManager) DownloadJobArtefact(ctx context.Context, jobName strin
83127
return
84128
}
85129

86-
artefactFilename := artefactManagerName
87-
if artefactManager.HasTitle() {
88-
artefactFilename = artefactManager.GetTitle()
89-
}
90-
if unescapedName, err := url.PathUnescape(artefactFilename); err == nil {
91-
artefactFilename = unescapedName
92-
}
93-
94130
expectedSizePtr, ok := artefactManager.GetSizeOk()
95131
if !ok {
96132
err = fmt.Errorf("%w: could not fetch artefact's size from artefact's manager [%v]", commonerrors.ErrUndefined, artefactManagerName)
@@ -105,6 +141,16 @@ func (m *ArtefactManager) DownloadJobArtefact(ctx context.Context, jobName strin
105141
}
106142
expectedHash := *expectedHashPtr
107143

144+
artefactFilename, artefactDestDir, err := determineArtefactDestination(outputDirectory, maintainTreeLocation, artefactManager)
145+
if err != nil {
146+
return
147+
}
148+
err = filesystem.MkDir(artefactDestDir)
149+
if err != nil {
150+
err = fmt.Errorf("%w: failed creating the output directory [%v] for job artefact: %v", commonerrors.ErrUnexpected, artefactDestDir, err.Error())
151+
return
152+
}
153+
108154
artefact, resp, apierr := m.getArtefactContentFunc(ctx, jobName, artefactManagerName)
109155
defer func() {
110156
if resp != nil {
@@ -120,7 +166,7 @@ func (m *ArtefactManager) DownloadJobArtefact(ctx context.Context, jobName strin
120166
return
121167
}
122168

123-
destination, err := filesystem.CreateFile(filepath.Join(outputDirectory, artefactFilename))
169+
destination, err := filesystem.CreateFile(filepath.Join(artefactDestDir, artefactFilename))
124170
if err != nil {
125171
err = fmt.Errorf("%w: could not create a location to store generated artefact [%v]: %v", commonerrors.ErrUnexpected, artefactFilename, err.Error())
126172
return
@@ -161,8 +207,11 @@ func (m *ArtefactManager) DownloadJobArtefact(ctx context.Context, jobName strin
161207
return
162208

163209
}
210+
func (m *ArtefactManager) DownloadJobArtefactFromLink(ctx context.Context, jobName string, outputDirectory string, artefactManagerItemLink *client.HalLinkData) error {
211+
return m.DownloadJobArtefactFromLinkWithTree(ctx, jobName, false, outputDirectory, artefactManagerItemLink)
212+
}
164213

165-
func (m *ArtefactManager) DownloadJobArtefactFromLink(ctx context.Context, jobName string, outputDirectory string, artefactManagerItem *client.HalLinkData) (err error) {
214+
func (m *ArtefactManager) DownloadJobArtefactFromLinkWithTree(ctx context.Context, jobName string, maintainTreeLocation bool, outputDirectory string, artefactManagerItemLink *client.HalLinkData) (err error) {
166215
err = parallelisation.DetermineContextError(ctx)
167216
if err != nil {
168217
return
@@ -171,12 +220,12 @@ func (m *ArtefactManager) DownloadJobArtefactFromLink(ctx context.Context, jobNa
171220
err = fmt.Errorf("%w: function to retrieve an artefact manager was not properly defined", commonerrors.ErrUndefined)
172221
return
173222
}
174-
if artefactManagerItem == nil {
223+
if artefactManagerItemLink == nil {
175224
err = fmt.Errorf("%w: missing artefact link", commonerrors.ErrUndefined)
176225
return
177226
}
178227

179-
artefactManagerName := artefactManagerItem.GetName()
228+
artefactManagerName := artefactManagerItemLink.GetName()
180229
artefactManager, resp, apierr := m.getArtefactManagerFunc(ctx, jobName, artefactManagerName)
181230
defer func() {
182231
if resp != nil {
@@ -190,7 +239,7 @@ func (m *ArtefactManager) DownloadJobArtefactFromLink(ctx context.Context, jobNa
190239
if resp != nil {
191240
_ = resp.Body.Close()
192241
}
193-
err = m.DownloadJobArtefact(ctx, jobName, outputDirectory, artefactManager)
242+
err = m.DownloadJobArtefactWithTree(ctx, jobName, maintainTreeLocation, outputDirectory, artefactManager)
194243
return
195244
}
196245

@@ -263,7 +312,11 @@ func (m *ArtefactManager) fetchJobArtefactsNextPage(ctx context.Context, current
263312
return
264313
}
265314

266-
func (m *ArtefactManager) DownloadAllJobArtefacts(ctx context.Context, jobName string, outputDirectory string) (err error) {
315+
func (m *ArtefactManager) DownloadAllJobArtefacts(ctx context.Context, jobName string, outputDirectory string) error {
316+
return m.DownloadAllJobArtefactsWithTree(ctx, jobName, false, outputDirectory)
317+
}
318+
319+
func (m *ArtefactManager) DownloadAllJobArtefactsWithTree(ctx context.Context, jobName string, maintainTreeStructure bool, outputDirectory string) (err error) {
267320
err = parallelisation.DetermineContextError(ctx)
268321
if err != nil {
269322
return
@@ -290,7 +343,7 @@ func (m *ArtefactManager) DownloadAllJobArtefacts(ctx context.Context, jobName s
290343
}
291344
artefactLink, ok := item.(*client.HalLinkData)
292345
if ok {
293-
subErr = m.DownloadJobArtefactFromLink(ctx, jobName, outputDirectory, artefactLink)
346+
subErr = m.DownloadJobArtefactFromLinkWithTree(ctx, jobName, maintainTreeStructure, outputDirectory, artefactLink)
294347
if subErr != nil {
295348
err = subErr
296349
return
@@ -299,7 +352,7 @@ func (m *ArtefactManager) DownloadAllJobArtefacts(ctx context.Context, jobName s
299352
} else {
300353
artefactManager, ok := item.(*client.ArtefactManagerItem)
301354
if ok {
302-
subErr = m.DownloadJobArtefact(ctx, jobName, outputDirectory, artefactManager)
355+
subErr = m.DownloadJobArtefactWithTree(ctx, jobName, maintainTreeStructure, outputDirectory, artefactManager)
303356
if subErr != nil {
304357
err = subErr
305358
return

utils/artefacts/artefacts_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/http"
1212
"os"
1313
"path/filepath"
14+
"strings"
1415
"testing"
1516
"time"
1617

@@ -164,6 +165,113 @@ func newTestArtefactManagerWithEmbeddedResources(t *testing.T, tmpDir, artefactC
164165
return newTestArtefactManager(t, tmpDir, artefactContent, false)
165166
}
166167

168+
func TestDetermineDestination(t *testing.T) {
169+
outputDir := strings.ReplaceAll(faker.Sentence(), " ", "//") + " "
170+
cleanedOutputDir := filepath.Clean(outputDir)
171+
172+
tests := []struct {
173+
item client.ArtefactManagerItem
174+
maintainTree bool
175+
outputDir string
176+
expectedFileName string
177+
expectedDir string
178+
}{
179+
{
180+
item: client.ArtefactManagerItem{
181+
ExtraMetadata: nil,
182+
Name: faker.Name(),
183+
Size: nil,
184+
Title: *client.NewNullableString(field.ToOptionalString("test.j")),
185+
},
186+
maintainTree: false,
187+
outputDir: outputDir,
188+
expectedFileName: "test.j",
189+
expectedDir: cleanedOutputDir,
190+
},
191+
{
192+
item: client.ArtefactManagerItem{
193+
ExtraMetadata: nil,
194+
Name: faker.Name(),
195+
Size: nil,
196+
Title: *client.NewNullableString(field.ToOptionalString("test.j")),
197+
},
198+
maintainTree: true,
199+
outputDir: outputDir,
200+
expectedFileName: "test.j",
201+
expectedDir: cleanedOutputDir,
202+
},
203+
{
204+
item: client.ArtefactManagerItem{
205+
ExtraMetadata: nil,
206+
Name: faker.Name(),
207+
Size: nil,
208+
Title: *client.NewNullableString(field.ToOptionalString("cool+blog&about%2Cstuff.yep")),
209+
},
210+
maintainTree: true,
211+
outputDir: outputDir,
212+
expectedFileName: "cool+blog&about,stuff.yep",
213+
expectedDir: cleanedOutputDir,
214+
},
215+
{
216+
item: client.ArtefactManagerItem{
217+
ExtraMetadata: &map[string]string{faker.Name(): faker.Sentence()},
218+
Name: faker.Name(),
219+
Size: nil,
220+
Title: *client.NewNullableString(field.ToOptionalString("cool+blog&about%2Cstuff.yep")),
221+
},
222+
maintainTree: true,
223+
outputDir: outputDir,
224+
expectedFileName: "cool+blog&about,stuff.yep",
225+
expectedDir: cleanedOutputDir,
226+
},
227+
{
228+
item: client.ArtefactManagerItem{
229+
ExtraMetadata: &map[string]string{relativePathKey: " test/1 "},
230+
Name: faker.Name(),
231+
Size: nil,
232+
Title: *client.NewNullableString(field.ToOptionalString("cool+blog&about%2Cstuff.yep")),
233+
},
234+
maintainTree: true,
235+
outputDir: outputDir,
236+
expectedFileName: "cool+blog&about,stuff.yep",
237+
expectedDir: filepath.Join(cleanedOutputDir, "test", "1"),
238+
},
239+
{
240+
item: client.ArtefactManagerItem{
241+
ExtraMetadata: &map[string]string{relativePathKey: " test/1/cool+blog&about,stuff.yep "},
242+
Name: faker.Name(),
243+
Size: nil,
244+
Title: *client.NewNullableString(field.ToOptionalString("cool+blog&about%2Cstuff.yep")),
245+
},
246+
maintainTree: true,
247+
outputDir: outputDir,
248+
expectedFileName: "cool+blog&about,stuff.yep",
249+
expectedDir: filepath.Join(cleanedOutputDir, "test", "1"),
250+
},
251+
{
252+
item: client.ArtefactManagerItem{
253+
ExtraMetadata: &map[string]string{relativePathKey: " test/1/cool+blog&about%2Cstuff.yep "},
254+
Name: faker.Name(),
255+
Size: nil,
256+
Title: *client.NewNullableString(field.ToOptionalString("cool+blog&about%2Cstuff.yep")),
257+
},
258+
maintainTree: true,
259+
outputDir: outputDir,
260+
expectedFileName: "cool+blog&about,stuff.yep",
261+
expectedDir: filepath.Join(cleanedOutputDir, "test", "1"),
262+
},
263+
}
264+
for i := range tests {
265+
test := tests[i]
266+
t.Run(fmt.Sprintf("%d_%s", i, test.expectedFileName), func(t *testing.T) {
267+
fileName, fileDest, err := determineArtefactDestination(test.outputDir, test.maintainTree, &test.item)
268+
require.NoError(t, err)
269+
assert.Equal(t, test.expectedFileName, fileName)
270+
assert.Equal(t, test.expectedDir, fileDest)
271+
})
272+
}
273+
}
274+
167275
func TestArtefactDownload(t *testing.T) {
168276
t.Run("Happy download artefact", func(t *testing.T) {
169277
tmpDir, err := filesystem.TempDirInTempDir("test-artefact-")
@@ -184,7 +292,25 @@ func TestArtefactDownload(t *testing.T) {
184292
require.NoError(t, err)
185293
assert.Equal(t, expectedContents, actualContents)
186294
})
295+
t.Run("Happy download artefact and keep tree", func(t *testing.T) {
296+
tmpDir, err := filesystem.TempDirInTempDir("test-artefact-with-tree-")
297+
require.NoError(t, err)
298+
defer func() { _ = filesystem.Rm(tmpDir) }()
299+
m, a := newTestArtefactManagerWithEmbeddedResources(t, tmpDir, faker.Sentence())
300+
301+
out := t.TempDir()
302+
err = m.DownloadJobArtefactFromLinkWithTree(context.Background(), faker.Word(), true, out, &client.HalLinkData{
303+
Name: &a.name,
304+
})
305+
require.NoError(t, err)
187306

307+
require.FileExists(t, filepath.Join(out, a.name))
308+
expectedContents, err := filesystem.ReadFile(a.path)
309+
require.NoError(t, err)
310+
actualContents, err := filesystem.ReadFile(filepath.Join(out, a.name))
311+
require.NoError(t, err)
312+
assert.Equal(t, expectedContents, actualContents)
313+
})
188314
t.Run("Happy list artefacts links", func(t *testing.T) {
189315
tmpDir, err := filesystem.TempDirInTempDir("test-artefact-")
190316
require.NoError(t, err)

utils/artefacts/interface.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,21 @@ import (
1515
//go:generate mockgen -destination=../mocks/mock_$GOPACKAGE.go -package=mocks github.com/ARM-software/embedded-development-services-client-utils/utils/$GOPACKAGE IArtefactManager
1616

1717
type IArtefactManager interface {
18-
// DownloadJobArtefactFromLink downloads a specific artefact into the output directory from a particular link.
18+
// DownloadJobArtefactFromLink downloads a specific artefact into the output directory from a particular link. The artefact will be placed at the root of the output directory.
1919
DownloadJobArtefactFromLink(ctx context.Context, jobName string, outputDirectory string, artefactManagerItemLink *client.HalLinkData) error
20-
// DownloadJobArtefact downloads a specific artefact into the output directory.
20+
// DownloadJobArtefactFromLinkWithTree downloads a specific artefact into the output directory from a particular link.
21+
// maintainTreeLocation specifies whether the artefact will be placed in a tree structure or if it will be flat.
22+
DownloadJobArtefactFromLinkWithTree(ctx context.Context, jobName string, maintainTreeLocation bool, outputDirectory string, artefactManagerItemLink *client.HalLinkData) error
23+
// DownloadJobArtefact downloads a specific artefact into the output directory. The artefact will be placed at the root of the output directory.
2124
DownloadJobArtefact(ctx context.Context, jobName string, outputDirectory string, artefactManager *client.ArtefactManagerItem) error
25+
// DownloadJobArtefactWithTree downloads a specific artefact into the output directory.
26+
// maintainTreeLocation specifies whether the artefact will be placed in a tree structure or if it will be flat.
27+
DownloadJobArtefactWithTree(ctx context.Context, jobName string, maintainTreeLocation bool, outputDirectory string, artefactManager *client.ArtefactManagerItem) error
2228
// ListJobArtefacts lists all artefact managers associated with a particular job.
2329
ListJobArtefacts(ctx context.Context, jobName string) (pagination.IPaginatorAndPageFetcher, error)
24-
// DownloadAllJobArtefacts downloads all the artefacts produced for a particular job and puts them in an output directory.
30+
// DownloadAllJobArtefacts downloads all the artefacts produced for a particular job and puts them in an output directory as a flat list.
2531
DownloadAllJobArtefacts(ctx context.Context, jobName string, outputDirectory string) error
32+
// DownloadAllJobArtefactsWithTree downloads all the artefacts produced for a particular job and puts them in an output directory.
33+
// maintainTreeStructure specifies whether to keep the tree structure of the artefacts or not in the output directory.
34+
DownloadAllJobArtefactsWithTree(ctx context.Context, jobName string, maintainTreeStructure bool, outputDirectory string) error
2635
}

utils/go.mod

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ module github.com/ARM-software/embedded-development-services-client-utils/utils
33
go 1.23
44

55
require (
6-
github.com/ARM-software/embedded-development-services-client/client v1.35.3
7-
github.com/ARM-software/golang-utils/utils v1.73.2
6+
github.com/ARM-software/embedded-development-services-client/client v1.36.1
7+
github.com/ARM-software/golang-utils/utils v1.74.1
88
github.com/go-faker/faker/v4 v4.5.0
99
github.com/go-logr/logr v1.4.2
1010
github.com/golang/mock v1.6.0
1111
github.com/stretchr/testify v1.9.0
1212
go.uber.org/atomic v1.11.0
1313
go.uber.org/goleak v1.3.0
14-
golang.org/x/sync v0.8.0
14+
golang.org/x/sync v0.9.0
1515
)
1616

1717
require (
@@ -68,10 +68,10 @@ require (
6868
github.com/zailic/slogr v0.0.2-alpha // indirect
6969
go.uber.org/multierr v1.10.0 // indirect
7070
go.uber.org/zap v1.27.0 // indirect
71-
golang.org/x/crypto v0.28.0 // indirect
71+
golang.org/x/crypto v0.29.0 // indirect
7272
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
73-
golang.org/x/sys v0.26.0 // indirect
74-
golang.org/x/text v0.19.0 // indirect
73+
golang.org/x/sys v0.27.0 // indirect
74+
golang.org/x/text v0.20.0 // indirect
7575
gopkg.in/ini.v1 v1.67.0 // indirect
7676
gopkg.in/yaml.v3 v3.0.1 // indirect
7777
)

0 commit comments

Comments
 (0)