| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990 |
- package web
- import (
- "bytes"
- "context"
- "errors"
- "os/exec"
- "strings"
- "time"
- )
- // Exit codes of the rekopub command, as documented in its README.
- const (
- exitFetch = 1
- exitNoContent = 3
- )
- // runResult is the outcome of one run of the rekopub command.
- type runResult struct {
- Path string // the saved book, from stdout
- Code int // exit code; -1 if the command didn't finish
- Err string // the command's last "error: " line, without the prefix
- }
- // runFunc runs the rekopub command with args. Tests replace it.
- type runFunc func(ctx context.Context, args []string) runResult
- // execRunner returns a runFunc that starts the command at cli. Each run is
- // its own process, so runs can't affect each other and a stuck one can be
- // killed. The command gets an empty environment: no proxy settings or
- // anything else from the server leak into it.
- func execRunner(cli string) runFunc {
- return func(ctx context.Context, args []string) runResult {
- cmd := exec.CommandContext(ctx, cli, args...)
- cmd.Env = []string{}
- cmd.WaitDelay = 5 * time.Second
- var stdout, stderr bytes.Buffer
- cmd.Stdout = &stdout
- cmd.Stderr = &stderr
- err := cmd.Run()
- res := runResult{Path: strings.TrimSpace(stdout.String()), Err: lastError(stderr.String())}
- var exit *exec.ExitError
- switch {
- case err == nil:
- case errors.As(err, &exit) && exit.Exited():
- res.Code = exit.ExitCode()
- default:
- res.Code = -1
- if res.Err == "" {
- res.Err = err.Error()
- }
- }
- return res
- }
- }
- // lastError returns the text of the last "error: " line in stderr.
- func lastError(stderr string) string {
- var msg string
- for line := range strings.Lines(stderr) {
- if rest, ok := strings.CutPrefix(line, "error: "); ok {
- msg = strings.TrimSpace(rest)
- }
- }
- return msg
- }
- // explain turns a failed run into a message for the visitor. Only fetch
- // errors pass on the command's own text, which describes the page (not
- // found, too large, not HTML and so on); the rest are fixed messages.
- func explain(ctx context.Context, res runResult) string {
- switch {
- case errors.Is(ctx.Err(), context.DeadlineExceeded):
- return "That page took too long to convert."
- case strings.Contains(res.Err, "local or private") || strings.Contains(res.Err, "-LOCAL"):
- return "That page leads to a local or private network address, which this site doesn't connect to."
- case res.Code == exitFetch && res.Err != "":
- msg := strings.TrimPrefix(res.Err, "couldn't fetch the page: ")
- if r := []rune(msg); len(r) > 300 {
- msg = string(r[:300]) + "…"
- }
- return "Couldn't download the page: " + msg
- case res.Code == exitFetch:
- return "Couldn't download the page."
- case res.Code == exitNoContent:
- return "No readable article was found on that page. Pages that build their content with JavaScript, paywalls and cookie walls can't be converted."
- default:
- return "Something went wrong while converting that page."
- }
- }
|