Skip to content

Commit

Permalink
refactor(pkg/godot/mirror)!: simplify Mirror usage by making it gen…
Browse files Browse the repository at this point in the history
…eric over `artifact.Versioned` (#128)

* refactor(pkg/godot/artifact/archive): require archive contents to be Versioned; impl Versioned

* refactor: update mirror to be generic over versioned artifacts

* refactor: simplify usage of download functions by making them generic

* fix: update install to use generic download function

* feat: add a name method to mirrors to improve logging

* refactor: implement streamlined mirror interfaces for GitHub and TuxFamily

* refactor: fix tests for mirrors

* refactor: improve checks for interface implementations

* fix comment to reflect new API

* chore: add a convenience method to make a mock artifact with a version

* feat: add tests for archive versions

* refactor: eliminate error path by panicking if a URL constant is invalid
  • Loading branch information
coffeebeats authored Nov 4, 2023
1 parent da762a2 commit 83fb6f1
Show file tree
Hide file tree
Showing 23 changed files with 447 additions and 924 deletions.
2 changes: 1 addition & 1 deletion internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ func execute(ctx context.Context, req *resty.Request, m, u string, h func(*resty
type silentLogger struct{}

// Compile-time verification that 'silentLogger' implements 'resty.Logger'.
var _ resty.Logger = silentLogger{}
var _ resty.Logger = (*silentLogger)(nil)

func (l silentLogger) Debugf(string, ...interface{}) {}
func (l silentLogger) Errorf(string, ...interface{}) {}
Expand Down
99 changes: 78 additions & 21 deletions pkg/download/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,90 @@ import (
"fmt"
"io/fs"
"os"
"path/filepath"

"github.com/charmbracelet/log"
"github.com/coffeebeats/gdenv/internal/client"
"github.com/coffeebeats/gdenv/pkg/godot/artifact"
"github.com/coffeebeats/gdenv/pkg/godot/mirror"
"github.com/coffeebeats/gdenv/pkg/progress"
)

type progressKey[T artifact.Versioned] struct{}

/* -------------------------------------------------------------------------- */
/* Function: WithProgress */
/* -------------------------------------------------------------------------- */

// WithProgress creates a sub-context with an associated progress reporter. The
// result can be passed to download functions in this package to get updates on
// the download progress for that specific artifact.
func WithProgress[T artifact.Versioned](
ctx context.Context,
p *progress.Progress,
) context.Context {
return context.WithValue(ctx, progressKey[T]{}, p)
}

/* -------------------------------------------------------------------------- */
/* Function: Download */
/* -------------------------------------------------------------------------- */

// Download uses the provided mirror to download the specified artifact and
// returns an 'artifact.Local' wrapper pointing to it.
func Download[T artifact.Versioned](
ctx context.Context,
a T,
out string,
) (artifact.Local[T], error) {
var local artifact.Local[T]

if err := checkIsDirectory(out); err != nil {
return local, err
}

m, err := mirror.Select(ctx, availableMirrors[T](), a)
if err != nil {
return local, err
}

log.Infof("downloading '%s' from mirror: %s", a.Name(), m.Name())

remote, err := m.Remote(a)
if err != nil {
return local, err
}

c := client.NewWithRedirectDomains(m.Hosts()...)

out = filepath.Join(out, remote.Artifact.Name())

p, ok := ctx.Value(progressKey[T]{}).(*progress.Progress)
if ok && p != nil {
ctx = client.WithProgress(ctx, p)
}

if err := c.DownloadTo(ctx, remote.URL, out); err != nil {
return local, err
}

log.Debugf("downloaded artifact: %s", out)

local.Artifact = remote.Artifact
local.Path = out

return local, nil
}

/* -------------------------------------------------------------------------- */
/* Function: availableMirrors */
/* -------------------------------------------------------------------------- */

// availableMirrors returns the list of possible 'Mirror' hosts.
func availableMirrors[T artifact.Versioned]() []mirror.Mirror[T] {
return []mirror.Mirror[T]{mirror.GitHub[T]{}, mirror.TuxFamily[T]{}}
}

/* -------------------------------------------------------------------------- */
/* Function: checkIsDirectory */
/* -------------------------------------------------------------------------- */
Expand All @@ -29,24 +107,3 @@ func checkIsDirectory(path string) error {

return nil
}

/* -------------------------------------------------------------------------- */
/* Function: downloadArtifact */
/* -------------------------------------------------------------------------- */

// downloadArtifact downloads an artifact and reports progress to the progress
// reporter extracted from the context using the provided key.
func downloadArtifact[T artifact.Artifact](
ctx context.Context,
c *client.Client,
a artifact.Remote[T],
out string,
progressKey any,
) error {
p, ok := ctx.Value(progressKey).(*progress.Progress)
if ok && p != nil {
ctx = client.WithProgress(ctx, p)
}

return c.DownloadTo(ctx, a.URL, out)
}
135 changes: 12 additions & 123 deletions pkg/download/executable.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,34 @@ package download

import (
"context"
"fmt"
"path/filepath"

"github.com/charmbracelet/log"
"github.com/coffeebeats/gdenv/internal/client"
"github.com/coffeebeats/gdenv/pkg/godot/artifact"
"github.com/coffeebeats/gdenv/pkg/godot/artifact/checksum"
"github.com/coffeebeats/gdenv/pkg/godot/artifact/executable"
"github.com/coffeebeats/gdenv/pkg/godot/mirror"
"github.com/coffeebeats/gdenv/pkg/godot/version"
"github.com/coffeebeats/gdenv/pkg/progress"
"golang.org/x/sync/errgroup"
)

type (
progressKeyExecutable struct{}
progressKeyExecutableChecksum struct{}

localExArchive = artifact.Local[executable.Archive]
localExChecksums = artifact.Local[checksum.Executable]
)

/* -------------------------------------------------------------------------- */
/* Function: WithExecutableProgress */
/* -------------------------------------------------------------------------- */

// WithSourceProgress creates a sub-context with an associated progress
// reporter. The result can be passed to download functions in this package to
// get updates on download progress.
func WithExecutableProgress(ctx context.Context, p *progress.Progress) context.Context {
return context.WithValue(ctx, progressKeyExecutable{}, p)
}

/* -------------------------------------------------------------------------- */
/* Function: WithExecutableChecksumProgress */
/* -------------------------------------------------------------------------- */

// WithExecutableChecksumProgress creates a sub-context with an associated
// progress reporter. The result can be passed to download functions in this
// package to get updates on download progress.
func WithExecutableChecksumProgress(ctx context.Context, p *progress.Progress) context.Context {
return context.WithValue(ctx, progressKeyExecutableChecksum{}, p)
}

/* -------------------------------------------------------------------------- */
/* Function: ExecutableWithChecksumValidation */
/* -------------------------------------------------------------------------- */

// ExecutableWithChecksumValidation downloads an executable archive and
// validates that its checksum matches the published value.
func ExecutableWithChecksumValidation(
ctx context.Context,
m mirror.Mirror,
ex executable.Executable,
out string,
) (artifact.Local[executable.Archive], error) {
chArchive, chChecksums := make(chan artifact.Local[executable.Archive]), make(chan artifact.Local[checksum.Executable])
defer close(chArchive)
defer close(chChecksums)

eg, downloadCtx := errgroup.WithContext(ctx)
eg, ctxDownload := errgroup.WithContext(ctx)

eg.Go(func() error {
result, err := Executable(downloadCtx, m, ex, out)
exArchive := executable.Archive{Artifact: ex}

result, err := Download(ctxDownload, exArchive, out)
if err != nil {
return err
}
Expand All @@ -78,7 +44,12 @@ func ExecutableWithChecksumValidation(
})

eg.Go(func() error {
result, err := ExecutableChecksums(downloadCtx, m, ex.Version(), out)
checksums, err := checksum.NewExecutable(ex.Version())
if err != nil {
return err
}

result, err := Download(ctxDownload, checksums, out)
if err != nil {
return err
}
Expand All @@ -104,85 +75,3 @@ func ExecutableWithChecksumValidation(

return exArchive, nil
}

/* -------------------------------------------------------------------------- */
/* Function: Executable */
/* -------------------------------------------------------------------------- */

// Executable downloads the Godot 'executable.Archive' for a specific version
// and platform and returns an 'artifact.Local' encapsulating the result.
func Executable(
ctx context.Context,
m mirror.Mirror,
ex executable.Executable,
out string,
) (localExArchive, error) {
if err := checkIsDirectory(out); err != nil {
return localExArchive{}, err
}

executableMirror, ok := m.(mirror.Executable)
if !ok || executableMirror == nil {
return localExArchive{}, fmt.Errorf("%w: executables", mirror.ErrNotSupported)
}

remote, err := executableMirror.ExecutableArchive(ex.Version(), ex.Platform())
if err != nil {
return localExArchive{}, err
}

c := client.NewWithRedirectDomains(m.Domains()...)

out = filepath.Join(out, remote.Artifact.Name())
if err := downloadArtifact(ctx, c, remote, out, progressKeyExecutable{}); err != nil {
return localExArchive{}, err
}

log.Debugf("downloaded executable: %s", out)

return localExArchive{
Artifact: remote.Artifact,
Path: out,
}, nil
}

/* -------------------------------------------------------------------------- */
/* Function: ExecutableChecksums */
/* -------------------------------------------------------------------------- */

// ExecutableChecksums downloads the Godot 'checksum.Source' file for a specific
// version and returns an 'artifact.Local' encapsulating the result.
func ExecutableChecksums(
ctx context.Context,
m mirror.Mirror,
v version.Version,
out string,
) (localExChecksums, error) {
if err := checkIsDirectory(out); err != nil {
return localExChecksums{}, err
}

executableMirror, ok := m.(mirror.Executable)
if !ok || executableMirror == nil {
return localExChecksums{}, fmt.Errorf("%w: executables", mirror.ErrNotSupported)
}

remote, err := executableMirror.ExecutableArchiveChecksums(v)
if err != nil {
return localExChecksums{}, err
}

c := client.NewWithRedirectDomains(m.Domains()...)

out = filepath.Join(out, remote.Artifact.Name())
if err := downloadArtifact(ctx, c, remote, out, progressKeyExecutableChecksum{}); err != nil {
return localExChecksums{}, err
}

log.Debugf("downloaded checksums file: %s", out)

return localExChecksums{
Artifact: remote.Artifact,
Path: out,
}, nil
}
Loading

0 comments on commit 83fb6f1

Please sign in to comment.