mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
proxy list and proxy delete sat next to serve, stats and mirror and read as cache operations. The commands are now proxy hosted push, proxy hosted list and proxy hosted delete, before a release makes the names hard to change. proxy hosted without a known subcommand prints the three. Refs #133
243 lines
7.3 KiB
Go
243 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultProxyURL = "http://localhost:8080"
|
|
hostedTokenEnv = "PROXY_HOSTED_TOKEN"
|
|
hostedClientTimeout = 10 * time.Minute
|
|
|
|
pushArgCount = 4 // ecosystem, name, version, file
|
|
listArgCount = 2 // ecosystem, name
|
|
deleteArgCount = 3 // ecosystem, name, version
|
|
tablePadding = 2
|
|
)
|
|
|
|
// hostedArtifact mirrors the JSON the /api/hosted endpoints return.
|
|
type hostedArtifact struct {
|
|
Version string `json:"version"`
|
|
Filename string `json:"filename"`
|
|
SHA256 string `json:"sha256"`
|
|
Size int64 `json:"size"`
|
|
PushedAt time.Time `json:"pushed_at"`
|
|
}
|
|
|
|
type hostedPushResponse struct {
|
|
Ecosystem string `json:"ecosystem"`
|
|
Name string `json:"name"`
|
|
hostedArtifact
|
|
}
|
|
|
|
type hostedListResponse struct {
|
|
Ecosystem string `json:"ecosystem"`
|
|
Name string `json:"name"`
|
|
Artifacts []hostedArtifact `json:"artifacts"`
|
|
}
|
|
|
|
type hostedCommand struct {
|
|
flags *flag.FlagSet
|
|
proxyURL *string
|
|
usage string
|
|
}
|
|
|
|
func newHostedCommand(name, usage, summary string, getenv func(string) string) hostedCommand {
|
|
fs := flag.NewFlagSet(name, flag.ContinueOnError)
|
|
fs.SetOutput(io.Discard)
|
|
proxyURL := getenv("PROXY_URL")
|
|
if proxyURL == "" {
|
|
proxyURL = defaultProxyURL
|
|
}
|
|
cmd := hostedCommand{flags: fs, usage: usage}
|
|
cmd.proxyURL = fs.String("url", proxyURL, "Proxy URL (env PROXY_URL)")
|
|
fs.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, "git-pkgs proxy - %s\n\nUsage: %s\n\nFlags:\n", summary, usage)
|
|
fs.SetOutput(os.Stderr)
|
|
fs.PrintDefaults()
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
// parse parses flags and checks for exactly want positional arguments.
|
|
func (c hostedCommand) parse(args []string, want int) ([]string, error) {
|
|
if err := c.flags.Parse(args); err != nil {
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
c.flags.Usage()
|
|
}
|
|
return nil, fmt.Errorf("%w\nusage: %s", err, c.usage)
|
|
}
|
|
if c.flags.NArg() != want {
|
|
return nil, fmt.Errorf("usage: %s", c.usage)
|
|
}
|
|
return c.flags.Args(), nil
|
|
}
|
|
|
|
func (c hostedCommand) endpoint(ecosystem, name string, version ...string) string {
|
|
segments := []string{url.PathEscape(ecosystem)}
|
|
for segment := range strings.SplitSeq(name, "/") {
|
|
segments = append(segments, url.PathEscape(segment))
|
|
}
|
|
for _, v := range version {
|
|
segments = append(segments, url.PathEscape(v))
|
|
}
|
|
return strings.TrimSuffix(*c.proxyURL, "/") + "/api/hosted/" + strings.Join(segments, "/")
|
|
}
|
|
|
|
func hostedToken(getenv func(string) string) (string, error) {
|
|
token := getenv(hostedTokenEnv)
|
|
if token == "" {
|
|
return "", fmt.Errorf("%s must be set to the proxy's hosted.token", hostedTokenEnv)
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
func runPush(args []string, getenv func(string) string, stdout io.Writer, client *http.Client) error {
|
|
cmd := newHostedCommand("push", "proxy hosted push [-url URL] <ecosystem> <name> <version> <file>",
|
|
"Push a package archive to the proxy", getenv)
|
|
positional, err := cmd.parse(args, pushArgCount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
token, err := hostedToken(getenv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ecosystem, name, version, path := positional[0], positional[1], positional[2], positional[3]
|
|
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
req, err := http.NewRequest(http.MethodPut, cmd.endpoint(ecosystem, name, version), file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Content-Type", "application/octet-stream")
|
|
|
|
var pushed hostedPushResponse
|
|
if err := doHosted(client, req, http.StatusCreated, &pushed); err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Fprintf(stdout, "pushed %s %s@%s as %s (%d bytes, sha256 %s)\n",
|
|
pushed.Ecosystem, pushed.Name, pushed.Version, pushed.Filename, pushed.Size, pushed.SHA256)
|
|
return err
|
|
}
|
|
|
|
func runList(args []string, getenv func(string) string, stdout io.Writer, client *http.Client) error {
|
|
cmd := newHostedCommand("list", "proxy hosted list [-url URL] <ecosystem> <name>", "List pushed package archives", getenv)
|
|
positional, err := cmd.parse(args, listArgCount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequest(http.MethodGet, cmd.endpoint(positional[0], positional[1]), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var listed hostedListResponse
|
|
if err := doHosted(client, req, http.StatusOK, &listed); err != nil {
|
|
return err
|
|
}
|
|
tw := tabwriter.NewWriter(stdout, 0, 0, tablePadding, ' ', 0)
|
|
_, _ = fmt.Fprintln(tw, "VERSION\tFILENAME\tSIZE\tSHA256\tPUSHED")
|
|
for _, a := range listed.Artifacts {
|
|
_, _ = fmt.Fprintf(tw, "%s\t%s\t%d\t%s\t%s\n", a.Version, a.Filename, a.Size, a.SHA256, a.PushedAt.Format(time.RFC3339))
|
|
}
|
|
return tw.Flush()
|
|
}
|
|
|
|
func runDelete(args []string, getenv func(string) string, stdout io.Writer, client *http.Client) error {
|
|
cmd := newHostedCommand("delete", "proxy hosted delete [-url URL] <ecosystem> <name> <version>", "Delete a pushed version", getenv)
|
|
positional, err := cmd.parse(args, deleteArgCount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
token, err := hostedToken(getenv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequest(http.MethodDelete, cmd.endpoint(positional[0], positional[1], positional[2]), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
if err := doHosted(client, req, http.StatusNoContent, nil); err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Fprintf(stdout, "deleted %s %s@%s\n", positional[0], positional[1], positional[2])
|
|
return err
|
|
}
|
|
|
|
// doHosted sends req and decodes a successful response into out. Any other
|
|
// status is returned as an error carrying the server's message.
|
|
func doHosted(client *http.Client, req *http.Request, wantStatus int, out any) error {
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != wantStatus {
|
|
var apiErr struct {
|
|
Message string `json:"message"`
|
|
}
|
|
if json.NewDecoder(resp.Body).Decode(&apiErr) == nil && apiErr.Message != "" {
|
|
return fmt.Errorf("%s %s: %s (%s)", req.Method, req.URL.Path, apiErr.Message, resp.Status)
|
|
}
|
|
return fmt.Errorf("%s %s: %s", req.Method, req.URL.Path, resp.Status)
|
|
}
|
|
if out == nil {
|
|
return nil
|
|
}
|
|
return json.NewDecoder(resp.Body).Decode(out)
|
|
}
|
|
|
|
const hostedUsage = "usage: proxy hosted <push|list|delete> [flags] ...\n\n" +
|
|
" push Push a package archive to the proxy\n" +
|
|
" list List pushed package archives\n" +
|
|
" delete Delete a pushed version"
|
|
|
|
type hostedSubcommand func([]string, func(string) string, io.Writer, *http.Client) error
|
|
|
|
var hostedSubcommands = map[string]hostedSubcommand{
|
|
"push": runPush,
|
|
"list": runList,
|
|
"delete": runDelete,
|
|
}
|
|
|
|
// runHosted runs the hosted subcommand named by args[0].
|
|
func runHosted(args []string, getenv func(string) string, stdout io.Writer, client *http.Client) error {
|
|
if len(args) == 0 {
|
|
return errors.New(hostedUsage)
|
|
}
|
|
run, ok := hostedSubcommands[args[0]]
|
|
if !ok {
|
|
return fmt.Errorf("unknown hosted command %q\n%s", args[0], hostedUsage)
|
|
}
|
|
return run(args[1:], getenv, stdout, client)
|
|
}
|
|
|
|
// runHostedCommand runs proxy hosted against the proxy's /api/hosted
|
|
// endpoints and exits non-zero on failure.
|
|
func runHostedCommand() {
|
|
client := &http.Client{Timeout: hostedClientTimeout}
|
|
if err := runHosted(os.Args[1:], os.Getenv, os.Stdout, client); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|