Skip to content
8 changes: 6 additions & 2 deletions .licenses/NOTICE.arduino-cli
Original file line number Diff line number Diff line change
Expand Up @@ -23378,9 +23378,9 @@ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

*****
go.bug.st/downloader/v2@v2.2.0
go.bug.st/downloader/v3@v3.0.0

Copyright (c) 2018, Cristian Maglie.
Copyright (c) 2018-2025, Cristian Maglie.
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -23412,6 +23412,10 @@ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

-----

This library is released under the permissive BSD-3-CLAUSE.

*****
go.bug.st/f@v0.4.0

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
---
name: go.bug.st/downloader/v2
version: v2.2.0
name: go.bug.st/downloader/v3
version: v3.0.0
type: go
summary:
homepage: https://pkg.go.dev/go.bug.st/downloader/v2
summary: Package downloader provides an asynchronous file downloader with progress
tracking and timeout support.
homepage: https://pkg.go.dev/go.bug.st/downloader/v3
license: bsd-3-clause
licenses:
- sources: LICENSE
text: |2+

Copyright (c) 2018, Cristian Maglie.
Copyright (c) 2018-2025, Cristian Maglie.
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -41,4 +42,6 @@ licenses:
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

- sources: README.md
text: This library is released under the permissive BSD-3-CLAUSE.
notices: []
2 changes: 1 addition & 1 deletion commands/internal/instances/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"github.com/arduino/arduino-cli/internal/version"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/go-paths-helper"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
)

// coreInstance is an instance of the Arduino Core Services. The user can
Expand Down
2 changes: 1 addition & 1 deletion commands/service_board_identify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"github.com/arduino/go-paths-helper"
"github.com/arduino/go-properties-orderedmap"
"github.com/stretchr/testify/require"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
semver "go.bug.st/relaxed-semver"
)

Expand Down
2 changes: 1 addition & 1 deletion commands/service_debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"github.com/arduino/go-properties-orderedmap"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
)

func TestGetCommandLine(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion commands/service_upload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
properties "github.com/arduino/go-properties-orderedmap"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
)

func TestDetectSketchNameFromBuildPath(t *testing.T) {
Expand Down
8 changes: 6 additions & 2 deletions debian/arduino-cli/usr/share/doc/arduino-cli/copyright
Original file line number Diff line number Diff line change
Expand Up @@ -23393,9 +23393,9 @@ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

*****
go.bug.st/downloader/v2@v2.2.0
go.bug.st/downloader/v3@v3.0.0

Copyright (c) 2018, Cristian Maglie.
Copyright (c) 2018-2025, Cristian Maglie.
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -23427,6 +23427,10 @@ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

-----

This library is released under the permissive BSD-3-CLAUSE.

*****
go.bug.st/f@v0.4.0

Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
[time.ParseDuration()](https://pkg.go.dev/time#ParseDuration), defaults to `720h` (30 days).
- `network` - configuration options related to the network connection.
- `proxy` - URL of the proxy server.
- `connection_timeout` - network connection timeout, the value format must be a valid input for
- `connection_timeout` - network inactivity timeout, the value format must be a valid input for
[time.ParseDuration()](https://pkg.go.dev/time#ParseDuration), defaults to `60s` (60 seconds). `0` means it will
wait indefinitely.
- `cloud_api.skip_board_detection_calls` - if set to `true` it will make the Arduino CLI skip network calls to Arduino
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/xeipuuv/gojsonschema v1.2.0
go.bug.st/cleanup v1.0.0
go.bug.st/downloader/v2 v2.2.0
go.bug.st/downloader/v3 v3.0.0
go.bug.st/f v0.4.0
go.bug.st/relaxed-semver v0.15.0
go.bug.st/testifyjson v1.3.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
go.bug.st/cleanup v1.0.0 h1:XVj1HZxkBXeq3gMT7ijWUpHyIC1j8XAoNSyQ06CskgA=
go.bug.st/cleanup v1.0.0/go.mod h1:EqVmTg2IBk4znLbPD28xne3abjsJftMdqqJEjhn70bk=
go.bug.st/downloader/v2 v2.2.0 h1:Y0jSuDISNhrzePkrAWqz9xUC3xol9hqZo/+tz1D4EqY=
go.bug.st/downloader/v2 v2.2.0/go.mod h1:VZW2V1iGKV8rJL2ZEGIDzzBeKowYv34AedJz13RzVII=
go.bug.st/downloader/v3 v3.0.0 h1:yXYamyDwQZonT8j2LD9i6hviqYQu7SuPEU1o5leMJak=
go.bug.st/downloader/v3 v3.0.0/go.mod h1:iwxwrxxREdx2e2BihdZZlX4uP15hq5DuIpwNHK7tjpk=
go.bug.st/f v0.4.0 h1:Vstqb950nMA+PhAlRxUw8QL1ntHy/gXHNyyzjkQLJ10=
go.bug.st/f v0.4.0/go.mod h1:bMo23205ll7UW63KwO1ut5RdlJ9JK8RyEEr88CmOF5Y=
go.bug.st/relaxed-semver v0.15.0 h1:w37+SYQPxF53RQO7QZZuPIMaPouOifdaP0B1ktst2nA=
Expand Down
2 changes: 1 addition & 1 deletion internal/arduino/cores/packagemanager/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"github.com/arduino/go-paths-helper"
"github.com/arduino/go-properties-orderedmap"
"github.com/stretchr/testify/require"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
semver "go.bug.st/relaxed-semver"
)

Expand Down
2 changes: 1 addition & 1 deletion internal/arduino/cores/packagemanager/package_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import (
properties "github.com/arduino/go-properties-orderedmap"
"github.com/arduino/go-timeutils"
"github.com/sirupsen/logrus"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
semver "go.bug.st/relaxed-semver"
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
"github.com/arduino/go-paths-helper"
"github.com/arduino/go-properties-orderedmap"
"github.com/stretchr/testify/require"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
"go.bug.st/f"
semver "go.bug.st/relaxed-semver"
)
Expand Down
25 changes: 6 additions & 19 deletions internal/arduino/httpclient/httpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ import (
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/go-paths-helper"
"github.com/sirupsen/logrus"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
)

// DownloadFile downloads a file from a URL into the specified path. An optional config and options may be passed (or nil to use the defaults).
// A DownloadProgressCB callback function must be passed to monitor download progress.
// If a not empty queryParameter is passed, it is appended to the URL for analysis purposes.
func DownloadFile(ctx context.Context, path *paths.Path, URL string, queryParameter string, label string, downloadCB rpc.DownloadProgressCB, config downloader.Config, options ...downloader.DownloadOptions) (returnedError error) {
func DownloadFile(ctx context.Context, path *paths.Path, URL string, queryParameter string, label string, downloadCB rpc.DownloadProgressCB, config downloader.Config) (returnedError error) {
if queryParameter != "" {
URL = URL + "?query=" + queryParameter
}
Expand All @@ -44,23 +44,10 @@ func DownloadFile(ctx context.Context, path *paths.Path, URL string, queryParame
}
}()

d, err := downloader.DownloadWithConfigAndContext(ctx, path.String(), URL, config, options...)
if err != nil {
return err
config.PollFunction = downloadCB.Update
config.PollInterval = 250 * time.Millisecond
if err := downloader.DownloadWithConfig(ctx, path.String(), URL, config); err != nil {
return &cmderrors.FailedDownloadError{Message: i18n.Tr("Download failed"), Cause: err}
}

err = d.RunAndPoll(func(downloaded int64) {
downloadCB.Update(downloaded, d.Size())
}, 250*time.Millisecond)
if err != nil {
return err
}

// The URL is not reachable for some reason
if d.Resp.StatusCode >= 400 && d.Resp.StatusCode <= 599 {
msg := i18n.Tr("Server responded with: %s", d.Resp.Status)
return &cmderrors.FailedDownloadError{Message: msg}
}

return nil
}
13 changes: 4 additions & 9 deletions internal/arduino/resources/checksums.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,7 @@ func (r *DownloadResource) TestLocalArchiveIntegrity(downloadDir *paths.Path) (b
return ok, nil
}

const (
filePermissions = 0644
packageFileName = "package.json"
)

type packageFile struct {
Checksum string `json:"checksum"`
}
const packageFileName = "package.json"

func computeDirChecksum(root string) (string, error) {
hash := sha256.New()
Expand Down Expand Up @@ -156,7 +149,9 @@ func CheckDirChecksum(root string) (bool, error) {
if err != nil {
return false, err
}
var file packageFile
var file struct {
Checksum string `json:"checksum"`
}
json.Unmarshal(packageJSON, &file)
checksum, err := computeDirChecksum(root)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/arduino/resources/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"github.com/arduino/arduino-cli/internal/i18n"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
paths "github.com/arduino/go-paths-helper"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
)

// Download performs a download loop using the provided downloader.Config.
Expand Down
8 changes: 5 additions & 3 deletions internal/arduino/resources/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
"github.com/arduino/go-paths-helper"
"github.com/codeclysm/extract/v4"
"github.com/sirupsen/logrus"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
)

// IndexResource is a reference to an index file URL with an optional signature.
Expand Down Expand Up @@ -79,7 +79,8 @@ func (res *IndexResource) Download(ctx context.Context, destDir *paths.Path, dow
return err
}
tmpIndexPath := tmp.Join(downloadFileName)
if err := httpclient.DownloadFile(ctx, tmpIndexPath, res.URL.String(), "", i18n.Tr("Downloading index: %s", downloadFileName), downloadCB, config, downloader.NoResume); err != nil {
config.DoNotResumeDownload = true // Disable resuming downloads for index files
if err := httpclient.DownloadFile(ctx, tmpIndexPath, res.URL.String(), "", i18n.Tr("Downloading index: %s", downloadFileName), downloadCB, config); err != nil {
Comment thread
cmaglie marked this conversation as resolved.
return &cmderrors.FailedDownloadError{Message: i18n.Tr("Error downloading index '%s'", res.URL), Cause: err}
}

Expand Down Expand Up @@ -134,7 +135,8 @@ func (res *IndexResource) Download(ctx context.Context, destDir *paths.Path, dow
// Download signature
signaturePath = destDir.Join(signatureFileName)
tmpSignaturePath = tmp.Join(signatureFileName)
if err := httpclient.DownloadFile(ctx, tmpSignaturePath, res.SignatureURL.String(), "", i18n.Tr("Downloading index signature: %s", signatureFileName), downloadCB, config, downloader.NoResume); err != nil {
config.DoNotResumeDownload = true // Disable resuming downloads for index files
if err := httpclient.DownloadFile(ctx, tmpSignaturePath, res.SignatureURL.String(), "", i18n.Tr("Downloading index signature: %s", signatureFileName), downloadCB, config); err != nil {
Comment thread
cmaglie marked this conversation as resolved.
return &cmderrors.FailedDownloadError{Message: i18n.Tr("Error downloading index signature '%s'", res.SignatureURL), Cause: err}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/arduino/resources/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
"github.com/arduino/go-paths-helper"
"github.com/stretchr/testify/require"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
)

func TestDownloadAndChecksums(t *testing.T) {
Expand Down
36 changes: 21 additions & 15 deletions internal/cli/configuration/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ import (
"strings"
"time"

"github.com/arduino/arduino-cli/commands/cmderrors"
"github.com/arduino/arduino-cli/internal/i18n"
"github.com/arduino/arduino-cli/internal/version"
"go.bug.st/downloader/v2"
"go.bug.st/downloader/v3"
"google.golang.org/grpc/metadata"
)

// UserAgent returns the user agent (mainly used by HTTP clients)
func (settings *Settings) UserAgent() string {
func (settings *Settings) UserAgent(ctx context.Context) string {
subComponent := ""
if settings != nil {
subComponent = settings.GetString("network.user_agent_ext")
Expand All @@ -48,6 +47,12 @@ func (settings *Settings) UserAgent() string {
extendedUA = " " + extendedUA
}

if md, ok := metadata.FromIncomingContext(ctx); ok {
if ctxUserAgent := strings.Join(md.Get("user-agent"), " "); ctxUserAgent != "" {
extendedUA += " " + ctxUserAgent
}
}

return fmt.Sprintf("%s/%s%s (%s; %s; %s) Commit:%s%s",
version.VersionInfo.Application,
version.VersionInfo.VersionString,
Expand Down Expand Up @@ -92,18 +97,12 @@ func (settings *Settings) NewHttpClient(ctx context.Context) (*http.Client, erro
if err != nil {
return nil, err
}
userAgent := settings.UserAgent()
if md, ok := metadata.FromIncomingContext(ctx); ok {
if extraUserAgent := strings.Join(md.Get("user-agent"), " "); extraUserAgent != "" {
userAgent += " " + extraUserAgent
}
}
return &http.Client{
Transport: &httpClientRoundTripper{
transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
userAgent: userAgent,
userAgent: settings.UserAgent(ctx),
},
Timeout: settings.ConnectionTimeout(),
}, nil
Expand All @@ -121,13 +120,20 @@ func (h *httpClientRoundTripper) RoundTrip(req *http.Request) (*http.Response, e

// DownloaderConfig returns the downloader configuration based on current settings.
func (settings *Settings) DownloaderConfig(ctx context.Context) (downloader.Config, error) {
httpClient, err := settings.NewHttpClient(ctx)
proxy, err := settings.NetworkProxy()
if err != nil {
return downloader.Config{}, &cmderrors.InvalidArgumentError{
Message: i18n.Tr("Could not connect via HTTP"),
Cause: err}
return downloader.Config{}, err
}

return downloader.Config{
HttpClient: *httpClient,
HttpClient: http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
},
InactivityTimeout: settings.ConnectionTimeout(),
ExtraHeaders: map[string]string{
"User-Agent": settings.UserAgent(ctx),
},
}, nil
}
10 changes: 9 additions & 1 deletion internal/cli/configuration/network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (

"github.com/arduino/arduino-cli/internal/cli/configuration"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata"
)

func TestUserAgentHeader(t *testing.T) {
Expand All @@ -34,9 +35,15 @@ func TestUserAgentHeader(t *testing.T) {
}))
defer ts.Close()

// Test user agent from gRPC metadata in context
md := metadata.New(map[string]string{"user-agent": "context-user-agent"})
ctx := metadata.NewIncomingContext(t.Context(), md)

// Test user agent from settings
settings := configuration.NewSettings()
require.NoError(t, settings.Set("network.user_agent_ext", "test-user-agent"))
client, err := settings.NewHttpClient(context.Background())

client, err := settings.NewHttpClient(ctx)
require.NoError(t, err)

request, err := http.NewRequest("GET", ts.URL, nil)
Expand All @@ -50,6 +57,7 @@ func TestUserAgentHeader(t *testing.T) {

fmt.Println("RESPONSE:", string(b))
require.Contains(t, string(b), "test-user-agent")
require.Contains(t, string(b), "context-user-agent")
}

func TestProxy(t *testing.T) {
Expand Down
Loading
Loading