Files
got/cli/help.go
Hojun-Cho 65104ade92 httpdl: fix progress-reporter race on name/out; harden mirror & resume validation
Findings from a Rob-Pike-lens review (bugs/races/network), each verified
against the code before fixing:

- httpdl: name/out are now atomic.Pointer[string] -- the engine publishes a
  download to the reporter before Run resolves the name, so Stat raced the
  write (bt already did this)
- httpdl: a malformed --proxy fails loudly instead of silently bypassing it
- httpdl: a 206 must carry a matching Content-Range; a 200 in segmented mode is
  fatal so it fails over instead of burning the retry budget
- httpdl: single-stream mirror failover validates the range before appending;
  ErrTooSlow only when the error is a real ctx cancellation
- httpdl: idle guard tracks progress by timestamp (no Reset/Stop race, no
  sticky fired flag)
- httpdl/control: reject a resume file whose segments don't tile [0,total)
- bt: clamp the listen-port range; verify on-disk data before choosing pieces
  under --check-integrity
- cli: reject size overflow; show --seed-time=MIN; clamp --select-file range
- progress/engine/main: clamp ETA against int64 overflow; show queued
  downloads as waiting; join the reporter on exit instead of a 20ms sleep
2026-06-20 23:28:26 +09:00

113 lines
2.8 KiB
Go

package cli
import (
"fmt"
"io"
"strings"
)
// Version is the program version, overridable at build time via -ldflags.
var Version = "0.1.0"
// PrintVersion writes the version banner.
func PrintVersion(w io.Writer) {
fmt.Fprintf(w, "got %s — a Rob-Pike-style command-line downloader in Go\n", Version)
}
// PrintHelp writes usage. A bare -h/--help (empty tag) shows only the Basic
// group, keeping the default terse; --help=all shows every group and
// --help=<group> (basic/http/bittorrent/advanced) filters to one.
func PrintHelp(w io.Writer, tag string) {
all := tag == "all"
// Empty tag means a bare -h/--help: default to the Basic group only.
bare := tag == ""
want, filtered := tagByName(tag)
if bare {
want, filtered = Basic, true
}
if !all && !bare && !filtered {
fmt.Fprintf(w, "got: unknown help tag %q (use all|basic|http|bittorrent|advanced)\n\n", tag)
}
fmt.Fprint(w, `Usage: got [OPTIONS] URI | magnet:... | file.torrent ...
A multi-protocol download tool: segmented HTTP(S) and BitTorrent.
`)
for _, group := range []Tag{Basic, HTTP, BitTorrent, Advanced} {
if filtered && !all && group != want {
continue
}
fmt.Fprintf(w, "%s options:\n", title(group.String()))
for i := range options {
o := &options[i]
if o.Tag != group {
continue
}
fmt.Fprintf(w, " %s %s\n", flagLabel(o), o.Help)
}
fmt.Fprintln(w)
}
// Only the full listing (all groups) repeats the meta flags.
if all || !filtered {
fmt.Fprint(w, " -h, --help[=TAG] show help (TAG: all|basic|http|bittorrent|advanced)\n")
fmt.Fprint(w, " -v, --version show version\n\n")
}
if bare {
fmt.Fprint(w, "Showing basic options. Use --help=all for every option, or --help=advanced for advanced ones.\n")
}
}
// title upper-cases the first letter of a single word.
func title(s string) string {
if s == "" {
return s
}
return strings.ToUpper(s[:1]) + s[1:]
}
func tagByName(name string) (Tag, bool) {
switch name {
case "basic":
return Basic, true
case "http":
return HTTP, true
case "bittorrent", "bt":
return BitTorrent, true
case "advanced":
return Advanced, true
default:
return Basic, false
}
}
// flagLabel renders the "-x, --long=ARG" column for one option.
func flagLabel(o *Opt) string {
var b strings.Builder
if o.Short != 0 {
fmt.Fprintf(&b, "-%c, ", o.Short)
} else {
b.WriteString(" ")
}
b.WriteString("--" + o.Long)
switch o.Kind {
case Bool:
// no argument shown
case Size:
b.WriteString("=SIZE")
case Int:
b.WriteString("=N")
case Float:
// seed-time is a count of minutes; only seed-ratio is an actual ratio.
if o.Long == "seed-time" {
b.WriteString("=MIN")
} else {
b.WriteString("=RATIO")
}
case Enum:
b.WriteString("=" + strings.Join(o.Enum, "|"))
default:
b.WriteString("=VAL")
}
return b.String()
}