You must login to view /gokrazy/tools/src/commit/ab669011325737dc654a82ce14d60c5476688dfc/internal.
The GitHub option should be usable for most people, it only links via username.

Files
tools/internal/gok/push.go
Michael Stapelberg 2c805ed001 refactor cobra command initialization to avoid stale state
Before this commit, we held onto *cobra.Command objects,
but that is not actually supported: after the first Execute(),
commands like updateCmd are stuck on the first-ever provided ctx.

Instead, turn command initialization into functions.

I only noticed this when trying to do two 'gok update'
from within the same test, where the fake build timestamp
is injected via the context (the timestamp was always the same).
2025-11-29 10:47:48 +01:00

92 lines
2.5 KiB
Go

package gok
import (
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/gokrazy/internal/instanceflag"
"github.com/spf13/cobra"
)
// pushCmd is gok push.
func pushCmd() *cobra.Command {
cmd := &cobra.Command{
GroupID: "server",
Use: "push",
Short: "Push a gokrazy image to a remote GUS server",
Long: `gok push pushes a local gaf (gokrazy archive format) file to a remote server.
When the --json flag is specified, the server response is printed to stdout.
Examples:
# push gokrazy.gaf to the GUS server at gus.gokrazy.org
% gok push --gaf /tmp/gokrazy.gaf --server https://gus.gokrazy.org
`,
RunE: func(cmd *cobra.Command, args []string) error {
return pushImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
},
}
cmd.Flags().StringVarP(&pushImpl.gafPath, "gaf", "", "", "path to the .gaf (gokrazy archive format) file to push to GUS (e.g. /tmp/gokrazy.gaf); build using gok overwrite --gaf")
cmd.Flags().StringVarP(&pushImpl.server, "server", "", "", "HTTP(S) URL to the server to push to")
cmd.Flags().BoolVarP(&pushImpl.json, "json", "", false, "print server JSON response directly to stdout")
instanceflag.RegisterPflags(cmd.Flags())
return cmd
}
type pushConfig struct {
gafPath string
server string
json bool
}
var pushImpl pushConfig
func (r *pushConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
// TODO: use an io.Reader that allows us to indicate progress
body, err := os.Open(r.gafPath)
if err != nil {
return err
}
defer body.Close()
st, err := body.Stat()
if err != nil {
return err
}
// TODO: fall back to the GUS server in the instance config if r.server == ""
start := time.Now()
url := strings.TrimSuffix(r.server, "/") + "/api/v1/push"
req, err := http.NewRequestWithContext(ctx, "PUT", url, body)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/octet-stream")
log.Printf("pushing %s (%d bytes) to %s", r.gafPath, st.Size(), url)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
if got, want := resp.StatusCode, http.StatusOK; got != want {
return fmt.Errorf("unexpected HTTP status: got %v, want %v", resp.Status, want)
}
if r.json {
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
return err
}
} else {
dur := time.Since(start)
log.Printf("uploaded in %v (%.f MB/s)",
dur.Truncate(1*time.Millisecond),
float64(st.Size()/1024/1024)/dur.Seconds())
}
return nil
}