diff options
| author | tsne <tsne.dev@outlook.com> | 2026-07-06 16:52:20 +0200 |
|---|---|---|
| committer | tsne <tsne.dev@outlook.com> | 2026-07-06 21:37:42 +0200 |
| commit | 84da43998f7cc42cc72652333ac6e3d293abb23e (patch) | |
| tree | 6610f82df854ae86f7f34152bb05fc712ef6f314 /internal/cli/errors.go | |
| download | hopper-main.tar.gz | |
Diffstat (limited to 'internal/cli/errors.go')
| -rw-r--r-- | internal/cli/errors.go | 89 |
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 } |