• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender-cli / 1807248277

08 May 2025 11:21AM UTC coverage: 1.701% (-0.04%) from 1.737%
1807248277

Pull #284

gitlab-ci

danielskinstad
chore: add input sanitization to devices list

Return error if page or per-page argument is less than 1

Signed-off-by: Daniel Skinstad Drabitzius <daniel.drabitzius@northern.tech>
Pull Request #284: feat: use paginated endpoint to list artifacts

0 of 64 new or added lines in 3 files covered. (0.0%)

4 existing lines in 2 files now uncovered.

45 of 2646 relevant lines covered (1.7%)

0.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/client/deployments/client.go
1
// Copyright 2023 Northern.tech AS
2
//
3
//        Licensed under the Apache License, Version 2.0 (the "License");
4
//        you may not use this file except in compliance with the License.
5
//        You may obtain a copy of the License at
6
//
7
//            http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//        Unless required by applicable law or agreed to in writing, software
10
//        distributed under the License is distributed on an "AS IS" BASIS,
11
//        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//        See the License for the specific language governing permissions and
13
//        limitations under the License.
14
package deployments
15

16
import (
17
        "archive/tar"
18
        "bytes"
19
        "encoding/json"
20
        "fmt"
21
        "io"
22
        "mime/multipart"
23
        "net/http"
24
        "net/http/httputil"
25
        "net/url"
26
        "os"
27
        "strconv"
28
        "strings"
29
        "time"
30

31
        "github.com/cheggaaa/pb/v3"
32
        "github.com/pkg/errors"
33

34
        "github.com/mendersoftware/mender-artifact/areader"
35

36
        "github.com/mendersoftware/mender-cli/client"
37
        "github.com/mendersoftware/mender-cli/log"
38
)
39

40
const (
41
        httpErrorBoundary = 300
42
)
43

44
type artifactData struct {
45
        ID                    string   `json:"id"`
46
        Description           string   `json:"description"`
47
        Name                  string   `json:"name"`
48
        DeviceTypesCompatible []string `json:"device_types_compatible"`
49
        Info                  struct {
50
                Format  string `json:"format"`
51
                Version int    `json:"version"`
52
        } `json:"info"`
53
        Signed  bool `json:"signed"`
54
        Updates []struct {
55
                TypeInfo struct {
56
                        Type string `json:"type"`
57
                } `json:"type_info"`
58
                Files []struct {
59
                        Name     string    `json:"name"`
60
                        Checksum string    `json:"checksum"`
61
                        Size     int       `json:"size"`
62
                        Date     time.Time `json:"date"`
63
                } `json:"files"`
64
                MetaData []interface{} `json:"meta_data"`
65
        } `json:"updates"`
66
        ArtifactProvides struct {
67
                ArtifactName string `json:"artifact_name"`
68
        } `json:"artifact_provides"`
69
        ArtifactDepends struct {
70
                DeviceType []string `json:"device_type"`
71
        } `json:"artifact_depends"`
72
        Size     int       `json:"size"`
73
        Modified time.Time `json:"modified"`
74
}
75

76
const (
77
        artifactUploadURL   = "/api/management/v1/deployments/artifacts"
78
        artifactsListURL    = "/api/management/v1/deployments/artifacts/list"
79
        artifactsDeleteURL  = artifactUploadURL
80
        directUploadURL     = "/api/management/v1/deployments/artifacts/directupload"
81
        transferCompleteURL = "/api/management/v1/deployments/artifacts/directupload/:id/complete"
82
        artifactURL         = "/api/management/v1/deployments/artifacts/:id"
83
        artifactDownloadURL = "/api/management/v1/deployments/artifacts/:id/download"
84
)
85

86
type Client struct {
87
        url                 string
88
        artifactUploadURL   string
89
        artifactURL         string
90
        artifactDownloadURL string
91
        artifactsListURL    string
92
        artifactDeleteURL   string
93
        directUploadURL     string
94
        client              *http.Client
95
}
96

97
type Link struct {
98
        Uri    string            `json:"uri"`
99
        Header map[string]string `json:"header,omitempty"`
100
}
101

102
type UploadLink struct {
103
        ArtifactID string `json:"id"`
104

105
        Link
106
}
107

108
func NewClient(url string, skipVerify bool) *Client {
×
109
        return &Client{
×
110
                url:                 url,
×
111
                artifactUploadURL:   client.JoinURL(url, artifactUploadURL),
×
112
                artifactURL:         client.JoinURL(url, artifactURL),
×
113
                artifactDownloadURL: client.JoinURL(url, artifactDownloadURL),
×
114
                artifactsListURL:    client.JoinURL(url, artifactsListURL),
×
115
                artifactDeleteURL:   client.JoinURL(url, artifactsDeleteURL),
×
116
                directUploadURL:     client.JoinURL(url, directUploadURL),
×
117
                client:              client.NewHttpClient(skipVerify),
×
118
        }
×
119
}
×
120

121
func (c *Client) DirectDownloadLink(token string) (*UploadLink, error) {
×
122
        var link UploadLink
×
123

×
124
        body, err := client.DoPostRequest(token, c.directUploadURL, c.client, nil)
×
125
        if err != nil {
×
126
                return nil, err
×
127
        }
×
128

129
        err = json.Unmarshal(body, &link)
×
130
        if err != nil {
×
131
                return nil, err
×
132
        }
×
133

134
        return &link, nil
×
135
}
136

NEW
137
func (c *Client) ListArtifacts(token string, detailLevel, perPage, page int, raw bool) error {
×
138
        if detailLevel > 3 || detailLevel < 0 {
×
139
                return fmt.Errorf("FAILURE: invalid artifact detail")
×
140
        }
×
141

NEW
142
        req, err := http.NewRequest(http.MethodGet, c.artifactsListURL, nil)
×
NEW
143
        if err != nil {
×
NEW
144
                return fmt.Errorf("failed to prepare request: %w", err)
×
NEW
145
        }
×
NEW
146
        req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
×
NEW
147
        q := url.Values{
×
NEW
148
                "per_page": []string{strconv.Itoa(perPage)},
×
NEW
149
                "page":     []string{strconv.Itoa(page)},
×
NEW
150
        }
×
NEW
151
        req.URL.RawQuery = q.Encode()
×
NEW
152

×
NEW
153
        reqDump, err := httputil.DumpRequest(req, false)
×
154
        if err != nil {
×
155
                return err
×
156
        }
×
NEW
157
        log.Verbf("sending request: \n%s", string(reqDump))
×
158

×
NEW
159
        rsp, err := c.client.Do(req)
×
160
        if err != nil {
×
161
                return err
×
162
        }
×
NEW
163
        defer rsp.Body.Close()
×
NEW
164
        if rsp.StatusCode != 200 {
×
NEW
165
                return fmt.Errorf("GET %s request failed with status %d",
×
NEW
166
                        req.URL.RequestURI(), rsp.StatusCode)
×
UNCOV
167
        }
×
168

NEW
169
        if raw {
×
NEW
170
                _, err := io.Copy(os.Stdout, rsp.Body)
×
NEW
171
                if err != nil {
×
NEW
172
                        return fmt.Errorf("error reading response body: %w", err)
×
NEW
173
                }
×
NEW
174
        } else {
×
NEW
175
                var list []artifactData
×
NEW
176
                err = json.NewDecoder(rsp.Body).Decode(&list)
×
NEW
177
                if err != nil {
×
NEW
178
                        return err
×
NEW
179
                }
×
NEW
180
                for _, v := range list {
×
NEW
181
                        listArtifact(v, detailLevel)
×
NEW
182
                }
×
183
        }
UNCOV
184
        return nil
×
185
}
186

187
func listArtifact(a artifactData, detailLevel int) {
×
188
        fmt.Printf("ID: %s\n", a.ID)
×
189
        fmt.Printf("Name: %s\n", a.Name)
×
190
        if detailLevel >= 1 {
×
191
                fmt.Printf("Signed: %t\n", a.Signed)
×
192
                fmt.Printf("Modfied: %s\n", a.Modified)
×
193
                fmt.Printf("Size: %d\n", a.Size)
×
194
                fmt.Printf("Description: %s\n", a.Description)
×
195
                fmt.Println("Compatible device types:")
×
196
                for _, v := range a.DeviceTypesCompatible {
×
197
                        fmt.Printf("  %s\n", v)
×
198
                }
×
199
                fmt.Printf("Artifact format: %s\n", a.Info.Format)
×
200
                fmt.Printf("Format version: %d\n", a.Info.Version)
×
201
        }
202
        if detailLevel >= 2 {
×
203
                fmt.Printf("Artifact provides: %s\n", a.ArtifactProvides.ArtifactName)
×
204
                fmt.Println("Artifact depends:")
×
205
                for _, v := range a.ArtifactDepends.DeviceType {
×
206
                        fmt.Printf("  %s\n", v)
×
207
                }
×
208
                fmt.Println("Updates:")
×
209
                for _, v := range a.Updates {
×
210
                        fmt.Printf("  Type: %s\n", v.TypeInfo.Type)
×
211
                        fmt.Println("  Files:")
×
212
                        for _, f := range v.Files {
×
213
                                fmt.Printf("\tName: %s\n", f.Name)
×
214
                                fmt.Printf("\tChecksum: %s\n", f.Checksum)
×
215
                                fmt.Printf("\tSize: %d\n", f.Size)
×
216
                                fmt.Printf("\tDate: %s\n", f.Date)
×
217
                                if len(v.Files) > 1 {
×
218
                                        fmt.Println()
×
219
                                }
×
220
                        }
221
                        if detailLevel == 3 {
×
222
                                fmt.Printf("  MetaData: %v\n", v.MetaData)
×
223
                        }
×
224
                }
225
        }
226

227
        fmt.Println("--------------------------------------------------------------------------------")
×
228
}
229

230
// Type info structure
231
type ArtifactUpdateTypeInfo struct {
232
        Type *string `json:"type" valid:"required"`
233
}
234

235
// Update file structure
236
type UpdateFile struct {
237
        // Image name
238
        Name string `json:"name" valid:"required"`
239

240
        // Image file checksum
241
        Checksum string `json:"checksum" valid:"optional"`
242

243
        // Image size
244
        Size int64 `json:"size" valid:"optional"`
245

246
        // Date build
247
        Date *time.Time `json:"date" valid:"optional"`
248
}
249

250
// Update structure
251
type Update struct {
252
        TypeInfo ArtifactUpdateTypeInfo `json:"type_info" valid:"required"`
253
        Files    []UpdateFile           `json:"files"`
254
        MetaData interface{}            `json:"meta_data,omitempty" valid:"optional"`
255
}
256

257
type DirectUploadMetadata struct {
258
        Size    int64    `json:"size,omitempty" valid:"-"`
259
        Updates []Update `json:"updates" valid:"-"`
260
}
261

262
func readArtifactMetadata(path string, size int64) io.Reader {
×
263
        log.Verbf("reading artifact file...")
×
264
        r, err := os.Open(path)
×
265
        if err != nil {
×
266
                log.Verbf("failed to open artifact file: %s", err.Error())
×
267
                return nil
×
268
        }
×
269
        defer r.Close()
×
270
        ar := areader.NewReader(r)
×
271
        err = ar.ReadArtifact()
×
272
        if err != nil {
×
273
                log.Verbf("failed to read artifact file: %s", err.Error())
×
274
                return nil
×
275
        }
×
276
        handlers := ar.GetHandlers()
×
277
        directUploads := make([]Update, len(handlers))
×
278
        for i, p := range handlers {
×
279
                files := p.GetUpdateAllFiles()
×
280
                if len(files) < 1 {
×
281
                        log.Verbf("the artifact has no files information")
×
282
                        return nil
×
283
                }
×
284
                for _, f := range files {
×
285
                        directUploads[i].Files = append(directUploads[i].Files, UpdateFile{
×
286
                                Name:     f.Name,
×
287
                                Checksum: string(f.Checksum),
×
288
                                Size:     f.Size,
×
289
                                Date:     &f.Date,
×
290
                        })
×
291
                }
×
292
        }
293
        directMetadata := DirectUploadMetadata{
×
294
                Size:    size,
×
295
                Updates: directUploads,
×
296
        }
×
297
        data, err := json.Marshal(directMetadata)
×
298
        if err != nil {
×
299
                log.Verbf("failed to parse artifact metadata: %s", err.Error())
×
300
                return nil
×
301
        }
×
302
        log.Verbf("done reading artifact file.")
×
303
        return bytes.NewBuffer(data)
×
304
}
305

306
func (c *Client) DirectUpload(
307
        token, artifactPath, id, url string,
308
        headers map[string]string,
309
        noProgress bool,
310
) error {
×
311
        var bar *pb.ProgressBar
×
312

×
313
        artifact, err := os.Open(artifactPath)
×
314
        if err != nil {
×
315
                return errors.Wrap(err, "Cannot read artifact file")
×
316
        }
×
317
        defer artifact.Close()
×
318

×
319
        artifactStats, err := artifact.Stat()
×
320
        if err != nil {
×
321
                return errors.Wrap(err, "Cannot read artifact file stats")
×
322
        }
×
323
        if err = checkArtifactFormat(artifact); err != nil {
×
324
                return err
×
325
        }
×
326
        var req *http.Request
×
327
        if !noProgress {
×
328
                // create progress bar
×
329
                bar = pb.New64(artifactStats.Size()).
×
330
                        Set(pb.Bytes, true).
×
331
                        SetRefreshRate(time.Millisecond * 100)
×
332
                bar.Start()
×
333
                req, err = http.NewRequest(http.MethodPut, url, bar.NewProxyReader(artifact))
×
334
        } else {
×
335
                req, err = http.NewRequest(http.MethodPut, url, artifact)
×
336
        }
×
337
        if err != nil {
×
338
                return errors.Wrap(err, "Cannot create request")
×
339
        }
×
340
        req.Header.Set("Content-Type", "application/vnd.mender-artifact")
×
341
        req.ContentLength = artifactStats.Size()
×
342

×
343
        reqDump, _ := httputil.DumpRequest(req, false)
×
344
        log.Verbf("sending request: \n%v", string(reqDump))
×
345

×
346
        for k, h := range headers {
×
347
                req.Header.Set(k, h)
×
348
        }
×
349
        rsp, err := c.client.Do(req)
×
350
        if err != nil {
×
351
                return errors.Wrap(err, "POST /artifacts request failed")
×
352
        }
×
353
        defer rsp.Body.Close()
×
354

×
355
        rspDump, _ := httputil.DumpResponse(rsp, true)
×
356
        log.Verbf("response: \n%v\n", string(rspDump))
×
357

×
358
        if rsp.StatusCode >= httpErrorBoundary {
×
359
                return errors.New(
×
360
                        fmt.Sprintf("artifact upload to '%s' failed with status %d", req.Host, rsp.StatusCode),
×
361
                )
×
362
        } else {
×
363
                body := readArtifactMetadata(artifactPath, artifactStats.Size())
×
364
                _, err := client.DoPostRequest(
×
365
                        token,
×
366
                        client.JoinURL(
×
367
                                c.url,
×
368
                                strings.ReplaceAll(transferCompleteURL, ":id", id),
×
369
                        ),
×
370
                        c.client,
×
371
                        body,
×
372
                )
×
373
                if err != nil {
×
374
                        return errors.Wrap(err, "failed to notify on complete upload")
×
375
                }
×
376
        }
377

378
        return nil
×
379
}
380

381
func (c *Client) UploadArtifact(
382
        description, artifactPath, token string,
383
        noProgress bool,
384
) error {
×
385
        var bar *pb.ProgressBar
×
386

×
387
        artifact, err := os.Open(artifactPath)
×
388
        if err != nil {
×
389
                return errors.Wrap(err, "Cannot read artifact file")
×
390
        }
×
391

392
        artifactStats, err := artifact.Stat()
×
393
        if err != nil {
×
394
                return errors.Wrap(err, "Cannot read artifact file stats")
×
395
        }
×
396

397
        if err = checkArtifactFormat(artifact); err != nil {
×
398
                return err
×
399
        }
×
400
        // create pipe
401
        pR, pW := io.Pipe()
×
402

×
403
        // create multipart writer
×
404
        writer := multipart.NewWriter(pW)
×
405

×
406
        req, err := http.NewRequest(http.MethodPost, c.artifactUploadURL, pR)
×
407
        if err != nil {
×
408
                return errors.Wrap(err, "Cannot create request")
×
409
        }
×
410
        req.Header.Set("Content-Type", writer.FormDataContentType())
×
411
        req.Header.Set("Authorization", "Bearer "+string(token))
×
412

×
413
        reqDump, _ := httputil.DumpRequest(req, false)
×
414
        log.Verbf("sending request: \n%v", string(reqDump))
×
415

×
416
        if !noProgress {
×
417
                // create progress bar
×
418
                bar = pb.New64(artifactStats.Size()).
×
419
                        Set(pb.Bytes, true).
×
420
                        SetRefreshRate(time.Millisecond * 100)
×
421
                bar.Start()
×
422
        }
×
423

424
        go func() {
×
425
                var part io.Writer
×
426
                defer pW.Close()
×
427
                defer artifact.Close()
×
428

×
429
                _ = writer.WriteField("size", strconv.FormatInt(artifactStats.Size(), 10))
×
430
                _ = writer.WriteField("description", description)
×
431
                part, _ = writer.CreateFormFile("artifact", artifactStats.Name())
×
432

×
433
                if !noProgress {
×
434
                        part = bar.NewProxyWriter(part)
×
435
                }
×
436

437
                if _, err := io.Copy(part, artifact); err != nil {
×
438
                        writer.Close()
×
439
                        _ = pR.CloseWithError(err)
×
440
                        return
×
441
                }
×
442

443
                writer.Close()
×
444
                if !noProgress {
×
445
                        log.Info("Processing uploaded file. This may take around one minute.\n")
×
446
                }
×
447
        }()
448

449
        rsp, err := c.client.Do(req)
×
450
        if err != nil {
×
451
                return errors.Wrap(err, "POST /artifacts request failed")
×
452
        }
×
453
        defer rsp.Body.Close()
×
454
        pR.Close()
×
455

×
456
        rspDump, _ := httputil.DumpResponse(rsp, true)
×
457
        log.Verbf("response: \n%v\n", string(rspDump))
×
458

×
459
        if rsp.StatusCode != http.StatusCreated {
×
460
                if rsp.StatusCode == http.StatusUnauthorized {
×
461
                        log.Verbf("artifact upload to '%s' failed with status %d", req.Host, rsp.StatusCode)
×
462
                        return errors.New("Unauthorized. Please Login first")
×
463
                } else if rsp.StatusCode == http.StatusConflict {
×
464
                        log.Verbf("artifact upload to '%s' failed with status %d", req.Host, rsp.StatusCode)
×
465
                        return errors.New("Artifact with same name or depends exists already")
×
466
                }
×
467
                return errors.New(
×
468
                        fmt.Sprintf("artifact upload to '%s' failed with status %d", req.Host, rsp.StatusCode),
×
469
                )
×
470
        }
471

472
        return nil
×
473
}
474

475
func (c *Client) DeleteArtifact(
476
        artifactID, token string,
477
) error {
×
478

×
479
        req, err := http.NewRequest(http.MethodDelete, c.artifactDeleteURL+"/"+artifactID, nil)
×
480
        if err != nil {
×
481
                return errors.Wrap(err, "Cannot create request")
×
482
        }
×
483
        req.Header.Set("Authorization", "Bearer "+string(token))
×
484

×
485
        reqDump, _ := httputil.DumpRequest(req, false)
×
486
        log.Verbf("sending request: \n%v", string(reqDump))
×
487

×
488
        rsp, err := c.client.Do(req)
×
489
        if err != nil {
×
490
                return errors.Wrap(err, "DELETE /artifacts request failed")
×
491
        }
×
492
        defer rsp.Body.Close()
×
493

×
494
        rspDump, _ := httputil.DumpResponse(rsp, true)
×
495
        log.Verbf("response: \n%v\n", string(rspDump))
×
496

×
497
        if rsp.StatusCode != http.StatusNoContent {
×
498
                body, err := io.ReadAll(rsp.Body)
×
499
                if err != nil {
×
500
                        return errors.Wrap(err, "can't read request body")
×
501
                }
×
502
                if rsp.StatusCode == http.StatusUnauthorized {
×
503
                        log.Verbf("artifact delete failed with status %d, reason: %s", rsp.StatusCode, body)
×
504
                        return errors.New("Unauthorized. Please Login first")
×
505
                }
×
506
                return errors.New(
×
507
                        fmt.Sprintf("artifact upload failed with status %d, reason: %s", rsp.StatusCode, body),
×
508
                )
×
509
        }
510

511
        return nil
×
512
}
513

514
func (c *Client) DownloadArtifact(
515
        sourcePath, artifactID, token string, noProgress bool,
516
) error {
×
517

×
518
        link, err := c.getLink(artifactID, token)
×
519
        if err != nil {
×
520
                return errors.Wrap(err, "Cannot get artifact link")
×
521
        }
×
522
        artifact, err := c.getArtifact(artifactID, token)
×
523
        if err != nil {
×
524
                return errors.Wrap(err, "Cannot get artifact details")
×
525
        }
×
526
        log.Verbf("link: \n%v\n", link.Uri)
×
527
        log.Verbf("artifact: \n%v\n", artifact.Size)
×
528

×
529
        if sourcePath != "" {
×
530
                sourcePath += "/"
×
531
        }
×
532
        sourcePath += artifact.Name + ".mender"
×
533

×
534
        req, err := http.NewRequest(http.MethodGet, link.Uri, nil)
×
535
        if err != nil {
×
536
                return errors.Wrap(err, "Cannot create request")
×
537
        }
×
538

539
        reqDump, _ := httputil.DumpRequest(req, false)
×
540
        log.Verbf("sending request: \n%v", string(reqDump))
×
541
        resp, err := c.client.Do(req)
×
542
        if err != nil {
×
543
                return errors.Wrap(err, "GET /artifacts request failed")
×
544
        }
×
545
        defer resp.Body.Close()
×
546

×
547
        switch resp.StatusCode {
×
548
        case http.StatusOK:
×
549
                return c.downloadFile(artifact.Size, sourcePath, resp, noProgress)
×
550
        case http.StatusBadRequest:
×
551
                return errors.New("Bad request\n")
×
552
        case http.StatusForbidden:
×
553
                return errors.New("Forbidden")
×
554
        case http.StatusNotFound:
×
555
                return errors.New("File not found on the device\n")
×
556
        case http.StatusConflict:
×
557
                return errors.New("The device is not connected\n")
×
558
        case http.StatusInternalServerError:
×
559
                return errors.New("Internal server error\n")
×
560
        default:
×
561
                return errors.New("Error: Received unexpected response code: " +
×
562
                        strconv.Itoa(resp.StatusCode))
×
563
        }
564
}
565

566
type DownloadLink struct {
567
        Uri    string    `json:"uri"`
568
        Expire time.Time `json:"expire"`
569
}
570

571
type Artifact struct {
572
        Size int64  `json:"size"`
573
        Name string `json:"name"`
574
}
575

576
func (c *Client) getArtifact(
577
        artifactID, token string,
578
) (*Artifact, error) {
×
579
        req, err := http.NewRequest(http.MethodGet,
×
580
                strings.ReplaceAll(c.artifactURL, ":id", artifactID), nil)
×
581
        if err != nil {
×
582
                return nil, errors.Wrap(err, "Cannot create request")
×
583
        }
×
584
        req.Header.Set("Authorization", "Bearer "+string(token))
×
585

×
586
        reqDump, _ := httputil.DumpRequest(req, false)
×
587
        log.Verbf("sending request: \n%v", string(reqDump))
×
588

×
589
        rsp, err := c.client.Do(req)
×
590
        if err != nil {
×
591
                return nil, errors.Wrap(err, "GET /artifacts request failed")
×
592
        }
×
593
        defer rsp.Body.Close()
×
594

×
595
        rspDump, _ := httputil.DumpResponse(rsp, true)
×
596
        log.Verbf("response: \n%v\n", string(rspDump))
×
597

×
598
        body, err := io.ReadAll(rsp.Body)
×
599
        if err != nil {
×
600
                return nil, errors.Wrap(err, "GET /artifacts request failed")
×
601
        }
×
602

603
        var artifact Artifact
×
604
        err = json.Unmarshal(body, &artifact)
×
605
        if err != nil {
×
606
                return nil, errors.Wrap(err, "GET /artifacts request failed")
×
607
        }
×
608

609
        return &artifact, nil
×
610
}
611

612
func (c *Client) getLink(
613
        artifactID, token string,
614
) (*DownloadLink, error) {
×
615
        req, err := http.NewRequest(http.MethodGet,
×
616
                strings.ReplaceAll(c.artifactDownloadURL, ":id", artifactID), nil)
×
617
        if err != nil {
×
618
                return nil, errors.Wrap(err, "Cannot create request")
×
619
        }
×
620
        req.Header.Set("Authorization", "Bearer "+string(token))
×
621

×
622
        reqDump, _ := httputil.DumpRequest(req, false)
×
623
        log.Verbf("sending request: \n%v", string(reqDump))
×
624

×
625
        rsp, err := c.client.Do(req)
×
626
        if err != nil {
×
627
                return nil, errors.Wrap(err, "GET /artifacts request failed")
×
628
        }
×
629
        defer rsp.Body.Close()
×
630

×
631
        rspDump, _ := httputil.DumpResponse(rsp, true)
×
632
        log.Verbf("response: \n%v\n", string(rspDump))
×
633

×
634
        body, err := io.ReadAll(rsp.Body)
×
635
        if err != nil {
×
636
                return nil, errors.Wrap(err, "GET /artifacts request failed")
×
637
        }
×
638

639
        var link DownloadLink
×
640
        err = json.Unmarshal(body, &link)
×
641
        if err != nil {
×
642
                return nil, errors.Wrap(err, "GET /artifacts request failed")
×
643
        }
×
644

645
        return &link, nil
×
646
}
647

648
func (c *Client) downloadFile(size int64, localFileName string, resp *http.Response,
649
        noProgress bool) error {
×
650
        path := resp.Header.Get("X-MEN-FILE-PATH")
×
651
        uid := resp.Header.Get("X-MEN-FILE-UID")
×
652
        gid := resp.Header.Get("X-MEN-FILE-GID")
×
653
        var n int64
×
654

×
655
        file, err := os.Create(localFileName)
×
656
        if err != nil {
×
657
                return errors.Wrap(err, "Cannot create file")
×
658
        }
×
659
        defer file.Close()
×
660
        if err != nil {
×
661
                log.Errf("Failed to create the file %s locally\n", path)
×
662
                return err
×
663
        }
×
664

665
        if resp.Header.Get("Content-Type") != "application/vnd.mender-artifact" {
×
666
                return fmt.Errorf("Unexpected Content-Type header: %s", resp.Header.Get("Content-Type"))
×
667
        }
×
668
        if err != nil {
×
669
                log.Err("downloadFile: Failed to parse the Content-Type header")
×
670
                return err
×
671
        }
×
672
        i, _ := strconv.Atoi(resp.Header.Get("Content-Length"))
×
673
        sourceSize := int64(i)
×
674
        source := resp.Body
×
675

×
676
        if !noProgress {
×
677
                var bar *pb.ProgressBar = pb.New64(sourceSize).
×
678
                        Set(pb.Bytes, true).
×
679
                        SetRefreshRate(time.Millisecond * 100)
×
680
                bar.Start()
×
681
                // create proxy reader
×
682
                reader := bar.NewProxyReader(source)
×
683
                n, err = io.Copy(file, reader)
×
684
        } else {
×
685
                n, err = io.Copy(file, source)
×
686
        }
×
687
        log.Verbf("wrote: %d\n", n)
×
688
        if err != nil {
×
689
                return err
×
690
        }
×
691
        if n != size {
×
692
                return errors.New(
×
693
                        "The downloaded file does not match the expected length in 'X-MEN-FILE-SIZE'",
×
694
                )
×
695
        }
×
696
        // Set the proper permissions and {G,U}ID's if present
697
        if uid != "" && gid != "" {
×
698
                uidi, err := strconv.Atoi(uid)
×
699
                if err != nil {
×
700
                        return err
×
701
                }
×
702
                gidi, err := strconv.Atoi(gid)
×
703
                if err != nil {
×
704
                        return err
×
705
                }
×
706
                err = os.Chown(file.Name(), uidi, gidi)
×
707
                if err != nil {
×
708
                        return err
×
709
                }
×
710
        }
711
        return nil
×
712
}
713
func checkArtifactFormat(artifact *os.File) error {
×
714
        tr := tar.NewReader(artifact)
×
715
        versionH, err := tr.Next()
×
716
        if err != nil {
×
717
                return errors.Wrap(err, "error parsing artifact")
×
718
        } else if versionH.Name != "version" {
×
719
                return errors.New("Invalid artifact format")
×
720
        }
×
721
        v := struct {
×
722
                Format string `json:"format"`
×
723
        }{}
×
724
        err = json.NewDecoder(tr).Decode(&v)
×
725
        if err != nil || v.Format != "mender" {
×
726
                return errors.New("Invalid artifact format")
×
727
        }
×
728
        _, err = artifact.Seek(0, io.SeekStart)
×
729
        if err != nil || v.Format != "mender" {
×
730
                return err
×
731
        }
×
732
        return nil
×
733
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc