package main import ( "bufio" "context" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "time" "git.warky.dev/wdevs/pgtidy/pkg/updatecheck" ) // windowsInstallerAsset is the release asset built by the NSIS installer step. const windowsInstallerAsset = "pgtidy-setup-windows-amd64.exe" // cmdUpdate implements `pgtidy update`: check the latest release and, on // Windows, offer to download and run the installer. func cmdUpdate(args []string, stdin io.Reader, stdout, stderr io.Writer) int { var checkOnly, assumeYes bool for _, a := range args { switch a { case "--check": checkOnly = true case "-y", "--yes": assumeYes = true case "-h", "--help": _, _ = fmt.Fprint(stdout, `pgtidy update — check for a newer release Usage: pgtidy update [--check] [-y|--yes] --check Only report whether an update is available -y, --yes Update without prompting On Windows the installer is downloaded and started; elsewhere the release page URL is shown. `) return 0 default: _, _ = fmt.Fprintf(stderr, "pgtidy update: unknown flag %q\n", a) return 2 } } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() err := runUpdate(ctx, updateOptions{ current: version, apiURL: updatecheck.DefaultAPIURL, goos: runtime.GOOS, checkOnly: checkOnly, assumeYes: assumeYes, in: stdin, out: stdout, install: downloadAndRunInstaller, }) if err != nil { _, _ = fmt.Fprintf(stderr, "pgtidy update: %v\n", err) return 1 } return 0 } type updateOptions struct { current string apiURL string goos string checkOnly bool assumeYes bool in io.Reader out io.Writer // install downloads and starts the installer found at url. install func(ctx context.Context, url string, out io.Writer) error } func runUpdate(ctx context.Context, o updateOptions) error { rel, err := updatecheck.Latest(ctx, nil, o.apiURL) if err != nil { return err } if !updatecheck.IsNewer(o.current, rel.Tag) { _, _ = fmt.Fprintf(o.out, "PgTidy %s is up to date (latest release: %s)\n", o.current, rel.Tag) return nil } _, _ = fmt.Fprintf(o.out, "A newer PgTidy is available: %s (installed: %s)\n", rel.Tag, o.current) if o.checkOnly { _, _ = fmt.Fprintf(o.out, "Release: %s\n", rel.URL) return nil } installer, canInstall := rel.FindAsset(windowsInstallerAsset) if o.goos != "windows" || !canInstall { _, _ = fmt.Fprintf(o.out, "Download it from: %s\n", rel.URL) return nil } if !o.assumeYes && !confirm(o.in, o.out, "Download and run the installer now?") { _, _ = fmt.Fprintf(o.out, "Skipped. Release: %s\n", rel.URL) return nil } return o.install(ctx, installer.URL, o.out) } // confirm asks a yes/no question, defaulting to no. func confirm(in io.Reader, out io.Writer, question string) bool { _, _ = fmt.Fprintf(out, "%s [y/N]: ", question) line, _ := bufio.NewReader(in).ReadString('\n') switch strings.ToLower(strings.TrimSpace(line)) { case "y", "yes": return true } return false } // downloadAndRunInstaller saves the installer to a temp directory and starts // it detached so this process can exit and release pgtidy.exe. func downloadAndRunInstaller(ctx context.Context, url string, out io.Writer) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("downloading installer: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("downloading installer: unexpected status %s", resp.Status) } dir, err := os.MkdirTemp("", "pgtidy-update-") if err != nil { return err } path := filepath.Join(dir, windowsInstallerAsset) f, err := os.Create(path) if err != nil { return err } if _, err := io.Copy(f, resp.Body); err != nil { _ = f.Close() return fmt.Errorf("downloading installer: %w", err) } if err := f.Close(); err != nil { return err } _, _ = fmt.Fprintf(out, "Starting installer: %s\n", path) return exec.Command(path).Start() }