aboutsummaryrefslogtreecommitdiff
path: root/internal/cli/errors.go
diff options
context:
space:
mode:
authortsne <tsne.dev@outlook.com>2026-07-06 16:52:20 +0200
committertsne <tsne.dev@outlook.com>2026-07-06 19:21:15 +0200
commit2c41acab4c97f584e8e199b31dc456194b8d7e6c (patch)
tree42381deb6c4f6da0f32945432207a31b4d86664e /internal/cli/errors.go
downloadhopper-2c41acab4c97f584e8e199b31dc456194b8d7e6c.tar.gz
initial
Diffstat (limited to 'internal/cli/errors.go')
-rw-r--r--internal/cli/errors.go89
1 files changed, 89 insertions, 0 deletions
diff --git a/internal/cli/errors.go b/internal/cli/errors.go
new file mode 100644
index 0000000..aeeede1
--- /dev/null
+++ b/internal/cli/errors.go
@@ -0,0 +1,89 @@
+package cli
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ "tsne.dev/hopper/internal/log"
+)
+
+// errorVerboser is implemented by error types that can contribute extra
+// technical detail to failure reports under --verbose (e.g. *bunny.apiError
+// exposing verb/path/status/cause). reportFailures prints it indented below the
+// friendly message when verbose is enabled.
+type errorVerboser interface {
+ ErrorVerbose() string
+}
+
+// usageError marks a parse or validation failure that should print a short
+// usage line and exit 2, distinct from a runtime failure (exit 1).
+type usageError struct {
+ msg string
+}
+
+func (e usageError) Error() string { return e.msg }
+
+// exitError signals that the command has already emitted all its user-facing
+// output (e.g. a failure list followed by the summary) and the process should
+// simply exit with the given code, printing nothing further. This keeps the
+// summary as the last line.
+type exitError struct {
+ code int
+}
+
+func (e exitError) Error() string { return fmt.Sprintf("exit status %d", e.code) }
+
+// reportFailures prints a batch of per-file failures to stderr under a headline,
+// using the same layout everywhere or the context's cancellation cause.
+// It returns true if it reported an error, false otherwise.
+func reportFailures(ctx context.Context, msg string, errs []error) bool {
+ if len(errs) > 0 {
+ sb := strings.Builder{}
+ sb.WriteString(msg)
+ for _, err := range errs {
+ sb.WriteString("\n ")
+ sb.WriteString(err.Error())
+ if log.Verbose() {
+ var v errorVerboser
+ if errors.As(err, &v) {
+ sb.WriteString("\n ")
+ sb.WriteString(v.ErrorVerbose())
+ }
+ }
+ }
+ log.Error(sb.String())
+ return true
+ } else if ctxErr := context.Cause(ctx); ctxErr != nil {
+ log.Error(ctxErr.Error())
+ return true
+ } else {
+ return false
+ }
+}
+
+// fileError attaches the storage path to an operation failure so the report can
+// name the offending file. It preserves the wrapped cause (e.g. the bunny API
+// error) in the chain so both friendly rendering (Error) and verbose detail
+// (ErrorVerbose) still work via errors.As.
+type fileError struct {
+ path string
+ err error
+}
+
+func (e fileError) Error() string { return e.path + ": " + e.err.Error() }
+func (e fileError) Unwrap() error { return e.err }
+
+type cancellationError struct {
+ msg string
+}
+
+// CancellationError returns an error that wraps `context.Canceled` but
+// with a custom error message.
+func CancellationError(msg string) error {
+ return cancellationError{msg}
+}
+
+func (e cancellationError) Error() string { return e.msg }
+func (e cancellationError) Unwrap() error { return context.Canceled }