Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cli/cmd/download_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ type DownloadPackageOpts struct {
Version string
Hash string
Filename string
Quiet bool
}

func (c *DownloadPackageCmd) RunE(_ *cobra.Command, args []string) error {
if c.Opts.Hash != "" {
if c.Opts.Hash != "" {
log.Printf("Downloading package '%s' with hash '%s'\n", c.Opts.Version, c.Opts.Hash)
} else {
log.Printf("Downloading package '%s'\n", c.Opts.Version)
Expand Down Expand Up @@ -66,6 +67,7 @@ func AddDownloadPackageCmd(download *cobra.Command, opts GlobalOptions) {
pkg.cmd.Flags().StringVarP(&pkg.Opts.Version, "version", "V", "", "Codesphere version to download")
pkg.cmd.Flags().StringVarP(&pkg.Opts.Hash, "hash", "H", "", "Hash of the version to download if multiple builds exist for the same version")
pkg.cmd.Flags().StringVarP(&pkg.Opts.Filename, "file", "f", "installer.tar.gz", "Specify artifact to download")
pkg.cmd.Flags().BoolVarP(&pkg.Opts.Quiet, "quiet", "q", false, "Suppress progress output during download")
download.AddCommand(pkg.cmd)

pkg.cmd.RunE = pkg.RunE
Expand All @@ -83,7 +85,7 @@ func (c *DownloadPackageCmd) DownloadBuild(p portal.Portal, build portal.Build,
}
defer func() { _ = out.Close() }()

err = p.DownloadBuildArtifact("codesphere", download, out)
err = p.DownloadBuildArtifact("codesphere", download, out, c.Opts.Quiet)
if err != nil {
return fmt.Errorf("failed to download build: %w", err)
}
Expand Down
3 changes: 2 additions & 1 deletion cli/cmd/download_package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var _ = Describe("ListPackages", func() {
Opts: cmd.DownloadPackageOpts{
Version: version,
Filename: filename,
Quiet: false,
},
FileWriter: mockFileWriter,
}
Expand Down Expand Up @@ -63,7 +64,7 @@ var _ = Describe("ListPackages", func() {

fakeFile := os.NewFile(uintptr(0), filename)
mockFileWriter.EXPECT().Create(version+"-"+filename).Return(fakeFile, nil)
mockPortal.EXPECT().DownloadBuildArtifact(portal.CodesphereProduct, expectedBuildToDownload, mock.Anything).Return(nil)
mockPortal.EXPECT().DownloadBuildArtifact(portal.CodesphereProduct, expectedBuildToDownload, mock.Anything, false).Return(nil)
err := c.DownloadBuild(mockPortal, build, filename)
Expect(err).NotTo(HaveOccurred())
})
Expand Down
2 changes: 1 addition & 1 deletion cli/cmd/update_oms.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (c *UpdateOmsCmd) SelfUpdate(p portal.Portal) error {
eg := errgroup.Group{}
eg.Go(func() error {
defer func() { _ = writer.Close() }()
err = p.DownloadBuildArtifact(portal.OmsProduct, download, writer)
err = p.DownloadBuildArtifact(portal.OmsProduct, download, writer, false)
if err != nil {
return fmt.Errorf("failed to download latest OMS package: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions cli/cmd/update_oms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ var _ = Describe("Update", func() {
mockVersion.EXPECT().Version().Return("0.0.0")
mockVersion.EXPECT().Os().Return("fakeos")
mockPortal.EXPECT().GetBuild(portal.OmsProduct, "", "").Return(latestBuild, nil)
mockPortal.EXPECT().DownloadBuildArtifact(portal.OmsProduct, buildToDownload, mock.Anything).RunAndReturn(
func(product portal.Product, build portal.Build, file io.Writer) error {
mockPortal.EXPECT().DownloadBuildArtifact(portal.OmsProduct, buildToDownload, mock.Anything, false).RunAndReturn(
func(product portal.Product, build portal.Build, file io.Writer, quiet bool) error {
embeddedFile, err := testdata.Open("testdata/testcli.tar.gz")
if err != nil {
Expect(err).NotTo(HaveOccurred())
Expand Down
14 changes: 9 additions & 5 deletions internal/portal/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
type Portal interface {
ListBuilds(product Product) (availablePackages Builds, err error)
GetBuild(product Product, version string, hash string) (Build, error)
DownloadBuildArtifact(product Product, build Build, file io.Writer) error
DownloadBuildArtifact(product Product, build Build, file io.Writer, quiet bool) error
RegisterAPIKey(owner string, organization string, role string, expiresAt time.Time) error
RevokeAPIKey(key string) error
UpdateAPIKey(key string, expiresAt time.Time) error
Expand Down Expand Up @@ -176,7 +176,7 @@ func (c *PortalClient) GetBuild(product Product, version string, hash string) (B
return matchingPackages[len(matchingPackages)-1], nil
}

func (c *PortalClient) DownloadBuildArtifact(product Product, build Build, file io.Writer) error {
func (c *PortalClient) DownloadBuildArtifact(product Product, build Build, file io.Writer, quiet bool) error {
reqBody, err := json.Marshal(build)
if err != nil {
return fmt.Errorf("failed to generate request body: %w", err)
Expand All @@ -188,10 +188,14 @@ func (c *PortalClient) DownloadBuildArtifact(product Product, build Build, file
}
defer func() { _ = resp.Body.Close() }()

// Create a WriteCounter to wrap the output file and report progress.
counter := NewWriteCounter(file)
// Create a WriteCounter to wrap the output file and report progress, unless quiet is requested.
// Default behavior: report progress. Quiet callers should pass true for quiet.
writer := file
if !quiet {
writer = NewWriteCounter(file)
}

_, err = io.Copy(counter, resp.Body)
_, err = io.Copy(writer, resp.Body)
if err != nil {
return fmt.Errorf("failed to copy response body to file: %w", err)
}
Expand Down
52 changes: 50 additions & 2 deletions internal/portal/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"io"
"log"
"net/http"
"net/url"
"time"
Expand All @@ -30,6 +31,29 @@ func NewFakeWriter() *FakeWriter {
return &FakeWriter{}
}

// slowReader splits the data into chunks and sleeps between reads to simulate chunked download
type slowReader struct {
data []byte
pos int
}

func (s *slowReader) Read(p []byte) (int, error) {
if s.pos >= len(s.data) {
return 0, io.EOF
}

// chunk size
chunk := 5
n := min(copy(p, s.data[s.pos:]), chunk)

// simulate delay for subsequent chunks so WriteCounter can trigger an update
if s.pos > 0 {
time.Sleep(150 * time.Millisecond)
}
s.pos += n
return n, nil
}

var _ = Describe("PortalClient", func() {
var (
client portal.PortalClient
Expand Down Expand Up @@ -186,18 +210,42 @@ var _ = Describe("PortalClient", func() {
getUrl = *req.URL
return &http.Response{
StatusCode: status,
Body: io.NopCloser(bytes.NewReader([]byte(downloadResponse))),
Body: io.NopCloser(&slowReader{data: []byte(downloadResponse)}),
}, nil
})
})

It("downloads the build", func() {
fakeWriter := NewFakeWriter()
err := client.DownloadBuildArtifact(product, build, fakeWriter)
err := client.DownloadBuildArtifact(product, build, fakeWriter, false)
Expect(err).NotTo(HaveOccurred())
Expect(fakeWriter.String()).To(Equal(downloadResponse))
Expect(getUrl.String()).To(Equal("fake-portal.com/packages/codesphere/download"))
})

It("emits progress logs when not quiet", func() {
var logBuf bytes.Buffer
prev := log.Writer()
log.SetOutput(&logBuf)
defer log.SetOutput(prev)

fakeWriter := NewFakeWriter()
err := client.DownloadBuildArtifact(product, build, fakeWriter, false)
Expect(err).NotTo(HaveOccurred())
Expect(logBuf.String()).To(ContainSubstring("Downloading..."))
})

It("does not emit progress logs when quiet", func() {
var logBuf bytes.Buffer
prev := log.Writer()
log.SetOutput(&logBuf)
defer log.SetOutput(prev)

fakeWriter := NewFakeWriter()
err := client.DownloadBuildArtifact(product, build, fakeWriter, true)
Expect(err).NotTo(HaveOccurred())
Expect(logBuf.String()).NotTo(ContainSubstring("Downloading..."))
})
})

Describe("GetLatestOmsBuild", func() {
Expand Down
19 changes: 10 additions & 9 deletions internal/portal/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions internal/portal/write_counter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) Codesphere Inc.
// SPDX-License-Identifier: Apache-2.0

package portal

import (
"bytes"
"log"
"testing"
"time"
)

func TestWriteCounterEmitsProgress(t *testing.T) {
// capture log output
var logBuf bytes.Buffer
prev := log.Writer()
log.SetOutput(&logBuf)
defer log.SetOutput(prev)

var underlying bytes.Buffer
wc := NewWriteCounter(&underlying)

// force an update by setting LastUpdate sufficiently in the past
wc.LastUpdate = time.Now().Add(-200 * time.Millisecond)

_, err := wc.Write([]byte("hello world"))
if err != nil {
t.Fatalf("write failed: %v", err)
}

out := logBuf.String()
if out == "" {
t.Fatalf("expected progress log output, got none")
}
}
3 changes: 1 addition & 2 deletions internal/util/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.